Singularity
API Reference

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

MethodParametersReturn TypeDescription
register(name, instance, path_segments, *, prefix, callables)name: str, instance: Any, path_segments: list[str], prefix: str = "/api", callables: dict[str, Callable] | None = NoneNoneRegister 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: strOptional[dict[str, Any]]Generate spec for a single service by name. Returns None if not found.
publish_to_redis(ttl)ttl: int = 60NoneAsync. 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)NoneAsync. Remove all local RPC specs from shared Redis.
discover_remote()(none)NoneAsync. 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: strboolAsync. Discover a single remote service from Redis on-demand. Returns True if found (locally or remotely), False otherwise.
get_proxy(caller)caller: strRPCProxyGet an RPC proxy bound to a caller identity.
resolve_service_for_path(path)path: strOptional[LocalServiceEntry]Find the local service mounted at a request path, using longest-prefix match. Used by RPCGuardMiddleware.
close()(none)NoneAsync. 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

MethodParametersReturn TypeDescription
__call__(caller)caller: strRPCProxyGet an RPC proxy bound to the given caller identity.
remote(service_name)service_name: strRemoteServiceDescriptorDeclare a remote service dependency as a class-level attribute. Returns a descriptor that resolves to a ServiceProxy at access time.

Module-Level Functions

FunctionParametersReturn TypeDescription
rpc(singleton)RPCClientPre-created module-level singleton. Import and call directly.
get_registry()(none)RPCRegistryGet 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

ParameterTypeDescription
callerstrThe name of the calling service
registryRPCRegistryThe RPC registry instance

Access Patterns

PatternReturnsDescription
proxy.paymentsServiceProxyDot access for standard service names
proxy["my-service"]ServiceProxyBracket 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

ParameterTypeDescription
service_namestrThe target service name
callerstrThe calling service name
registryRPCRegistryThe RPC registry instance

Dispatch Behavior

When you access a method on a ServiceProxy, it resolves using this priority:

  1. Local -- If service_name exists in registry._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.
  2. Remote (cached) -- If service_name exists in registry._remote, checks the blacklist, verifies the method exists, and returns an async callable that makes an HTTP request.
  3. 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

AspectValue
HTTP ClientPooled httpx.AsyncClient (keep-alive connections reused across calls)
Timeoutsettings.rpc_timeout (default 30.0 seconds)
HeadersX-RPC-Caller: {caller}, plus X-API-Key: {settings.rpc_api_key} when set
Request MethodDetermined by the remote method spec (GET, POST, etc.)
ArgumentsEncoded by transport.encode_arguments -- see Argument Encoding

Internal Methods

MethodParametersReturn TypeDescription
_check_blacklist(blacklist)blacklist: list[str]NoneRaises 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

ParameterTypeDescription
service_namestrThe 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

AttributeTypeDefaultDescription
host_urlstr""Base URL of the remote deployment (e.g. "http://localhost:8001")
api_keystr""API key sent as X-API-Key header (optional)
timeoutfloat30.0Request timeout in seconds
prefixstr"/api"API prefix mounted on the remote repo
service_namestr""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__ ReturnsUsage Pattern
"" (empty)MicroserviceProxyself.billing.invoice.post(...) -- pick service, then method
"test" (set)MicroserviceServiceProxyself.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 result

MicroserviceProxy

singularity.rpc.proxy.MicroserviceProxy

Proxy representing a remote deployment, bound to a caller identity. Attribute access returns a MicroserviceServiceProxy for the named service.

Constructor

ParameterTypeDescription
host_urlstrBase URL of the remote deployment
api_keystrAPI key for authentication
timeoutfloatRequest timeout in seconds
callerstrThe calling service name

Access Patterns

PatternReturnsDescription
proxy.invoiceMicroserviceServiceProxyDot access for service names
proxy["my-service"]MicroserviceServiceProxyBracket 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

ParameterTypeDescription
service_namestrThe target service name
host_urlstrBase URL of the remote deployment
api_keystrAPI key for authentication
timeoutfloatRequest timeout in seconds
callerstrThe 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 NameHTTP MethodURL
getGET{host_url}{prefix}/{service_name}
postPOST{host_url}{prefix}/{service_name}
get_exampleGET{host_url}{prefix}/{service_name}/example
post_order_paymentPOST{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

AspectValue
HTTP Clienthttpx.AsyncClient
TimeoutValue from Microservice.timeout
HeadersX-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 passedSent 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.

ControlDefaultBehavior
BlacklistAlways onA 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 keyOff (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.
ParameterTypeDescription
registryRPCRegistryRegistry used to map request paths to services
require_api_keyboolReject requests without a valid key
api_keysfrozenset[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

ExceptionConstructor ParametersFieldsWhen Raised
RPCError(standard Exception args)messageBase class; not raised directly
RPCServiceNotFoundservice: strserviceTarget service not found in local or remote registry
RPCMethodNotFoundservice: str, method: strservice, methodMethod not found on the target service
RPCAccessDeniedcaller: str, target: strcaller, targetCaller is in the target service's blacklist
RPCServiceUnavailableservice: str, url: str, detail: str = ""service, urlThe request never produced a response -- connection refused, DNS failure, or timeout
RPCRemoteErrorservice: str, url: str, status_code: int, payload: Any = Noneservice, url, status_code, payloadThe remote service answered with a non-2xx status. payload holds the decoded body

Error Messages

ExceptionMessage 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.

FieldTypeDefaultDescription
paramsdict(required)Parameter metadata: {name: {"type": str, "required": bool}}
return_typestr(required)String representation of the return type annotation
descriptionstr(required)First line of the method's docstring
is_asyncbool(required)Whether the method is a coroutine
http_methodstr"POST"HTTP method (GET, POST, PUT, DELETE, PATCH)
sub_pathstr""Sub-path for http_exposed routes (e.g. "status" for get=status)

LocalServiceEntry

A locally registered RPC service. Uses __slots__.

FieldTypeDescription
instanceAnyThe service instance
methodsdict[str, MethodMeta]Introspected method metadata
blacklistlist[str]Services blocked from calling this service
endpoint_prefixstrAbsolute URL prefix advertised to remote callers (e.g. "http://localhost:8000/api/v1/payments")
route_prefixstrLocal path the service is mounted at (e.g. "/api/v1/payments")
path_segmentslist[str]Directory path segments (e.g. ["v1", "payments"])
callablesdict[str, Callable]Hook-wrapped endpoints by method name, used for local dispatch

RemoteServiceEntry

A remotely discovered RPC service. Uses __slots__.

FieldTypeDescription
base_urlstrBase URL of the remote deployment
blacklistlist[str]Services blocked from calling this service
methodsdict[str, Any]Method specs as raw dictionaries from Redis