AsterClient
AsterClient Usage
Section titled “AsterClient Usage”AsterClient is the consumer-side counterpart to AsterServer. It handles the admission handshake and RPC client construction behind a single async context manager, with all configuration driven by AsterConfig (environment variables or TOML file).
Quick start
Section titled “Quick start”import asynciofrom aster import AsterClientfrom my_services import HelloService, HelloRequest
async def main(): async with AsterClient() as c: hello = await c.client(HelloService) resp = await hello.say_hello(HelloRequest(name="World")) print(resp.message)
asyncio.run(main())ASTER_ENDPOINT_ADDR=<from producer output> python consumer.pyThat’s it in dev mode. AsterClient reads configuration from ASTER_* environment variables automatically.
Constructor
Section titled “Constructor”AsterClient( config=AsterConfig.from_env(), # auto-loaded when omitted identity=".aster-identity", # optional: path to identity file peer="billing-consumer", # optional: select peer from identity file address="aster1...", # producer's address root_pubkey=b'\x01\x02...', # override: root public key enrollment_credential_file="cred.json", # override: credential path channel_name="rpc", # optional (default: "rpc"))All arguments are optional when the corresponding ASTER_* env vars or .aster-identity file are set.
peer parameter
Section titled “peer parameter”Selects a consumer peer entry from the .aster-identity file by name. When present, AsterClient loads the node secret key (for stable EndpointId), the root public key, and the signed enrollment credential from the identity file automatically.
# With identity file:async with AsterClient(identity=".aster-identity", peer="billing-consumer") as c: svc = await c.client(MyService) ...If identity is omitted, AsterClient auto-detects .aster-identity in the current directory. If peer is omitted, it auto-selects the first [[peers]] entry with role = "consumer". If no identity file is found, the client runs in dev mode.
Configuration resolution (each field: inline kwarg > identity file > env var > TOML > default)
Section titled “Configuration resolution (each field: inline kwarg > identity file > env var > TOML > default)”| What | Source | Purpose |
|---|---|---|
| Producer address | ASTER_ENDPOINT_ADDR or endpoint_addr= |
Required. Base64 NodeAddr or EndpointId hex string. |
| Enrollment credential | .aster-identity [[peers]] entry or ASTER_ENROLLMENT_CREDENTIAL |
Signed token for admission. Not needed in dev mode. |
| Root public key | .aster-identity root_pubkey field, ASTER_ROOT_PUBKEY, or ASTER_ROOT_PUBKEY_FILE |
For validating the producer’s root key in the admission response. |
| Node identity | .aster-identity [node].secret_key or ASTER_SECRET_KEY |
32-byte key for stable EndpointId. Ephemeral if unset. |
| IID token | ASTER_ENROLLMENT_CREDENTIAL_IID |
Cloud Instance Identity Document (when credential requires IID). |
Dev mode vs production
Section titled “Dev mode vs production”Dev mode (no .aster-identity, producer has allow_all_consumers=True):
ASTER_ENDPOINT_ADDR=<addr> python consumer.pyProduction with identity file (recommended):
ASTER_ENDPOINT_ADDR=<addr> python consumer.py# .aster-identity in cwd provides the credential automaticallyProduction in containers (identity file at a custom path):
ASTER_IDENTITY_FILE=/etc/aster/.aster-identity \ASTER_ENDPOINT_ADDR=<addr> \python consumer.pyLegacy approach (credential file without identity file – still supported):
ASTER_ENDPOINT_ADDR=<addr> \ASTER_ENROLLMENT_CREDENTIAL=consumer.token \python consumer.pyLifecycle
Section titled “Lifecycle”async with AsterClient() as c:
Section titled “async with AsterClient() as c:”The context manager calls connect() on entry and close() on exit.
connect()
Section titled “connect()”Always runs the admission handshake (to discover available services):
connect() 1. Create a QUIC endpoint (with ASTER_SECRET_KEY for stable identity if set) 2. Connect to producer on the aster.consumer_admission ALPN 3. If enrollment credential is configured: - Load from file, send with ConsumerAdmissionRequest - Producer verifies signature, expiry, EndpointId binding 4. If no credential (dev mode): - Send empty request; producer auto-admits if allow_all_consumers=True 5. Receive ConsumerAdmissionResponse with services list 6. If admitted=false -> raise PermissionError 7. Store the services listAfter connect(), inspect what services the producer offers:
async with AsterClient() as c: for s in c.services: print(f"{s.name} v{s.version} contract_id={s.contract_id[:16]}...")close()
Section titled “close()”Closes all RPC connections and the underlying QUIC endpoint. Safe to call multiple times.
Getting an RPC client
Section titled “Getting an RPC client”hello = await c.client(HelloService)client() takes a @service-decorated class and returns a typed ServiceClient with method stubs matching the service definition. Under the hood it:
- Reads
__aster_service_info__from the class (name, version, methods). - Looks up the service in the admission response’s
ServiceSummarylist. - Opens (or reuses) a QUIC connection to the service’s RPC channel on
aster/1. - Creates a
ServiceClientwithIrohTransportandForyCodec.
Method signature
Section titled “Method signature”await c.client( service_cls, # required: the @service-decorated class channel="rpc", # optional: override channel name codec=ForyCodec(...), # optional: custom codec interceptors=[...], # optional: client-side interceptors)Making RPC calls
Section titled “Making RPC calls”Once you have a client, call methods directly:
Unary RPC
Section titled “Unary RPC”hello = await c.client(HelloService)resp = await hello.say_hello(HelloRequest(name="World"))print(resp.message)Server streaming
Section titled “Server streaming”stocks = await c.client(StockService)async for update in stocks.watch_price(TickerRequest(symbol="AAPL")): print(f"${update.price}")Client streaming
Section titled “Client streaming”uploader = await c.client(UploadService)
async def chunks(): for data in read_file_chunks("big_file.bin"): yield Chunk(data=data)
result = await uploader.upload(chunks())Bidirectional streaming
Section titled “Bidirectional streaming”chat_svc = await c.client(ChatService)channel = chat_svc.chat()async with channel: await channel.send(ChatMsg(text="hello")) response = await channel.recv() print(response.text)Connection reuse
Section titled “Connection reuse”Multiple .client() calls to services at the same address reuse the same QUIC connection:
async with AsterClient() as c: hello = await c.client(HelloService) # opens connection to producer tasks = await c.client(TaskService) # reuses same connectionError handling
Section titled “Error handling”Admission denied
Section titled “Admission denied”try: async with AsterClient() as c: ...except PermissionError as e: print(f"Admission denied: {e}") # "consumer admission denied — set ASTER_ENROLLMENT_CREDENTIAL to a valid enrollment token"This happens when: no credential is set and the producer requires one; the credential is invalid/expired; or the root public key doesn’t match.
No endpoint address
Section titled “No endpoint address”try: async with AsterClient() as c: # ASTER_ENDPOINT_ADDR not set ...except ValueError as e: print(e) # "AsterClient requires an endpoint address. Set ASTER_ENDPOINT_ADDR..."Service not found
Section titled “Service not found”try: client = await c.client(UnknownService)except LookupError as e: print(e) # "service 'UnknownService' v1 not offered by producer..."RPC errors
Section titled “RPC errors”from aster import RpcError, StatusCode
try: resp = await hello.say_hello(HelloRequest(name=""))except RpcError as e: print(f"RPC failed: {e.code} - {e.message}")Complete production example
Section titled “Complete production example”import asynciofrom aster import AsterClientfrom my_services import HelloService, HelloRequest
async def main(): # peer= selects the consumer credential from .aster-identity async with AsterClient(identity=".aster-identity", peer="billing-consumer") as c: print(f"Admitted! Services:") for s in c.services: print(f" {s.name} v{s.version} (contract: {s.contract_id[:16]}...)")
hello = await c.client(HelloService) resp = await hello.say_hello(HelloRequest(name="Aster")) print(f"Response: {resp.message}")
asyncio.run(main())# Production: .aster-identity provides credential + node identityASTER_ENDPOINT_ADDR=<producer addr> python consumer.py
# Or in a container with a custom identity file path:ASTER_IDENTITY_FILE=/etc/aster/.aster-identity \ASTER_ENDPOINT_ADDR=<producer addr> \python consumer.pySequence diagram
Section titled “Sequence diagram”Diagram source
sequenceDiagram
participant C as Consumer (AsterClient)
participant P as Producer (AsterServer)
rect rgba(120, 166, 255, 0.05)
Note over C,P: Admission phase
C->>P: QUIC connect (aster.consumer_admission ALPN)
C->>P: ConsumerAdmissionRequest (credential JSON)
P->>P: verify credential (or auto-admit)
P->>P: add peer to Gate 0 allowlist
P-->>C: ConsumerAdmissionResponse (services list)
end
rect rgba(97, 214, 194, 0.05)
Note over C,P: RPC phase
C->>P: QUIC connect (aster/1 ALPN)
C->>P: StreamHeader (service, method)
C->>P: Request frame
P-->>C: Response frame
P-->>C: OK trailer
end