Package: pyrum
pyrumPyrum ("Py Rumor") is a Python interface to interact withRumor, an Eth2 networking shell.UsageThis interface maps async Python functions (built on Trio) to Rumor commands.The mapping is simple:A command path is equal to a series of fieldsA command argument is equal to a call argumentA command flag is equal to a call keyword argument, every_in the keyword is replaced with-Some examples:host listen --tcp=9001
->
peer.listen(tcp=9001)

peer connect /dns4/prylabs.net/tcp/30001/p2p/16Uiu2HAm7Qwe19vz9WzD2Mxn7fXd1vgHHp4iccuyq7TxwRXoAGfc
->
peer.connect('/dns4/prylabs.net/tcp/30001/p2p/16Uiu2HAm7Qwe19vz9WzD2Mxn7fXd1vgHHp4iccuyq7TxwRXoAGfc')Each of these calls returns aCallobject:Wait for successful call completion by awaiting themy_call.Retrieve latest data from themy_call.datadictIndividual data can be retrieved by callingawait my_call.some_data_field()to get the respective data as soon as it is available.If you expect multiple occurrences of the data, you canawaitmultiple times, or useasync for item in my_callFull exampleimporttriofrompyrumimportWebsocketConn,TCPConn,UnixConn,SubprocessConn,Rumorfromremerkleable.compleximportContainerfromremerkleable.byte_arraysimportBytes32,Bytes4fromremerkleable.basicimportuint64classStatusReq(Container):version:Bytes4finalized_root:Bytes32finalized_epoch:uint64head_root:Bytes32head_slot:uint64asyncdefbasic_rpc_example(rumor:Rumor):alice=rumor.actor('alice')awaitalice.host.start()# Flags are keyword argumentsawaitalice.host.listen(tcp=9000)print("started alice")# Concurrency in Rumor, planned with async (Trio) in Pyrumbob=rumor.actor('bob')awaitbob.host.start()awaitbob.host.listen(tcp=9001)print("started bob")long_call=alice.sleep('5s')# sleep 5 secondsprint('made long call')short_call=alice.sleep('3s')# sleep 3 secondsprint('made short call')awaitshort_callprint("done with short call")awaitlong_callprint('done with long call')# Getting a result should be as easy as calling, and waiting for the key we are afterbob_addr=awaitbob.host.view().addr()print('BOB has address: ',bob_addr)# Command arguments are just call argumentsawaitalice.peer.connect(bob_addr)print("connected alice to bob!")# You can use either await or async-for to get data of a specific keyasyncforaddrinbob.host.view().addr():print(f'bob has addr:{addr}')# multiple addresses, but the last one matters most.# Optionally request more peer detailspeerdata=awaitalice.peer.list(details=True).peers()print(f'alice peer list:{peerdata}')print("Testing a Status RPC exchange")alice_peer_id=awaitalice.host.view().peer_id()alice_status=StatusReq(head_slot=42)bob_status=StatusReq(head_slot=123)asyncdefalice_work(nursery:trio.Nursery)->Call:print("alice: listening for status requests")call=alice.rpc.status.listen(raw=True)# Wait for inital completion of setup, i.e. the listener is online.# It will stay open in the background, until `await call.finished()`awaitcallasyncdefprocess_requests():# Each req object is a dict with all the latest log data at the time of completion of the step.asyncforreqincall:print(f"alice: Got request:{req}")assert'input_err'notinreq# Or send back an error; await alice.rpc.status.resp.invalid_request(req['req_id'], f"hello! Your request was invalid, because: {req['input_err']}")assertreq['data']==bob_status.encode_bytes().hex()resp=alice_status.encode_bytes().hex()print(f"alice: sending response back to request{req['req_id']}:{resp}")awaitalice.rpc.status.resp.chunk.raw(req['req_id'],resp,done=True)print("alice: stopped listening for status requests")nursery.start_soon(process_requests)returncallasyncdefbob_work():# Send alice a status requestreq=bob_status.encode_bytes().hex()print(f"bob: sending alice ({alice_peer_id}) a status request:{req}")req_call=bob.rpc.status.req.raw(alice_peer_id,req,raw=True)awaitreq_call# Await request to be written to streamawaitreq_call.next()# Await first (and only) response chunkresp=awaitreq_call.next()print(f"bob: received status response from alice:{resp}")assertresp['chunk_index']==0# first chunkassertresp['result_code']==0# success chunkassertresp['data']==alice_status.encode_bytes().hex()# Run tasks in a trio nursery to make them concurrentasyncwithtrio.open_nursery()asnursery:# Set up alice to listen for requestsalice_listen_call=awaitalice_work(nursery)# Make bob send a request and check a response, after alice is set upawaitbob_work()# Close listener of aliceawaitalice_listen_call.cancel()asyncdefrun_example():# Websockets# Start Rumor with websocket serving enabled, then open a connection from rumor:# rumor serve --ws=localhost:8010 --ws-path=/ws --ws-key=foobar# async with WebsocketConn(ws_url='ws://localhost:8010/ws', ws_key='foobar') as conn:# TCP sockets# rumor serve --tcp localhost:3030# async with TCPConn(addr='localhost', port=3030) as conn:# Unix domain sockets# rumor serve --ipc my_ipc.socket# async with UnixConn(socket_path='../some/path/my_ipc.socket') as conn:# Subprocess# Run it in "bare" mode so there is no shell clutter, and every Rumor output is JSON for Pyrum to parse.# Optionally specify your own rumor executable, for local development/modding of RumorasyncwithSubprocessConn(cmd='cd ../rumor && go run . bare --level=trace')asconn:# A Trio nursery hosts all the async tasks of the Rumor instance.asyncwithtrio.open_nursery()asnursery:# And optionally use Rumor(conn, nursery, debug=True) to be super verbose about Rumor communication.awaitbasic_rpc_example(Rumor(conn,nursery,debug=True))trio.run(run_example)Example Output:started alice
started bob
made long call
made short call
done with short call
done with long call
BOB has address:  /ip4/127.0.0.1/tcp/9001/p2p/16Uiu2HAm4ZYRU2J9pmsCLnYSfmGEQSsCCAaHD5aXr2MheSUf7egu
connected alice to bob!
bob has addr: /ip4/127.0.0.1/tcp/9001/p2p/16Uiu2HAm4ZYRU2J9pmsCLnYSfmGEQSsCCAaHD5aXr2MheSUf7egu
bob has addr: /ip4/192.168.0.77/tcp/9001/p2p/16Uiu2HAm4ZYRU2J9pmsCLnYSfmGEQSsCCAaHD5aXr2MheSUf7egu
alice peer list: {'16Uiu2HAm4ZYRU2J9pmsCLnYSfmGEQSsCCAaHD5aXr2MheSUf7egu': {'peer_id': '16Uiu2HAm4ZYRU2J9pmsCLnYSfmGEQSsCCAaHD5aXr2MheSUf7egu', 'node_id': '384cbbccd148b82a96138d552e6909225d4169bfaf0b1ea27ccd10e02141187e', 'pubkey': '0287bd489f112f9d43bc04b7c561df3b93d35f1311b6d52df524321b7485e2012e', 'addrs': ['/ip4/127.0.0.1/tcp/9001', '/ip4/192.168.0.77/tcp/9001'], 'protocols': ['/p2p/id/delta/1.0.0', '/ipfs/ping/1.0.0', '/libp2p/circuit/relay/0.1.0', '/ipfs/id/1.0.0', '/ipfs/id/push/1.0.0'], 'user_agent': 'Rumor', 'protocol_version': 'ipfs/0.1.0'}}
Testing a Status RPC exchange
alice: listening for status requests
bob: sending alice (16Uiu2HAm6QTKBBzwpPGt1R8SQHHrUex3czzhd23VnbDkVztwibgD) a status request: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b00000000000000
alice: Got request: {'msg': 'Received request, queued it to respond to!', 'data': '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b00000000000000', 'from': '16Uiu2HAm4ZYRU2J9pmsCLnYSfmGEQSsCCAaHD5aXr2MheSUf7egu', 'protocol': '/eth2/beacon_chain/req/status/1/ssz_snappy', 'req_id': 0}
alice: sending response back to request 0: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a00000000000000
bob: received status response from alice: {'msg': 'Received chunk', 'chunk_index': 0, 'chunk_size': 84, 'data': '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a00000000000000', 'from': '16Uiu2HAm6QTKBBzwpPGt1R8SQHHrUex3czzhd23VnbDkVztwibgD', 'protocol': '/eth2/beacon_chain/req/status/1/ssz_snappy', 'result_code': 0}LicenseMIT, seeLICENSEfile.