Skip to content

Commit 7754db0

Browse files
authored
Merge pull request #55 from rocklabs-io/async_request
add async function
2 parents a87c60b + a8616c1 commit 7754db0

File tree

6 files changed

+222
-8
lines changed

6 files changed

+222
-8
lines changed

ic/agent.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,26 @@ def query_endpoint(self, canister_id, data):
3737
ret = self.client.query(canister_id, data)
3838
return cbor2.loads(ret)
3939

40+
async def query_endpoint_async(self, canister_id, data):
41+
ret = await self.client.query_async(canister_id, data)
42+
return cbor2.loads(ret)
43+
4044
def call_endpoint(self, canister_id, request_id, data):
4145
self.client.call(canister_id, request_id, data)
4246
return request_id
4347

48+
async def call_endpoint_async(self, canister_id, request_id, data):
49+
await self.client.call_async(canister_id, request_id, data)
50+
return request_id
51+
4452
def read_state_endpoint(self, canister_id, data):
4553
result = self.client.read_state(canister_id, data)
4654
return result
4755

56+
async def read_state_endpoint_async(self, canister_id, data):
57+
result = await self.client.read_state_async(canister_id, data)
58+
return result
59+
4860
def query_raw(self, canister_id, method_name, arg, return_type = None, effective_canister_id = None):
4961
req = {
5062
'request_type': "query",
@@ -61,6 +73,22 @@ def query_raw(self, canister_id, method_name, arg, return_type = None, effective
6173
elif result['status'] == 'rejected':
6274
return result['reject_message']
6375

76+
async def query_raw_async(self, canister_id, method_name, arg, return_type = None, effective_canister_id = None):
77+
req = {
78+
'request_type': "query",
79+
'sender': self.identity.sender().bytes,
80+
'canister_id': Principal.from_str(canister_id).bytes if isinstance(canister_id, str) else canister_id.bytes,
81+
'method_name': method_name,
82+
'arg': arg,
83+
'ingress_expiry': self.get_expiry_date()
84+
}
85+
_, data = sign_request(req, self.identity)
86+
result = await self.query_endpoint_async(canister_id if effective_canister_id is None else effective_canister_id, data)
87+
if result['status'] == 'replied':
88+
return decode(result['reply']['arg'], return_type)
89+
elif result['status'] == 'rejected':
90+
return result['reject_message']
91+
6492
def update_raw(self, canister_id, method_name, arg, return_type = None, effective_canister_id = None, **kwargs):
6593
req = {
6694
'request_type': "call",
@@ -82,7 +110,26 @@ def update_raw(self, canister_id, method_name, arg, return_type = None, effectiv
82110
else:
83111
raise Exception('Timeout to poll result, current status: ' + str(status))
84112

85-
113+
async def update_raw_async(self, canister_id, method_name, arg, return_type = None, effective_canister_id = None, **kwargs):
114+
req = {
115+
'request_type': "call",
116+
'sender': self.identity.sender().bytes,
117+
'canister_id': Principal.from_str(canister_id).bytes if isinstance(canister_id, str) else canister_id.bytes,
118+
'method_name': method_name,
119+
'arg': arg,
120+
'ingress_expiry': self.get_expiry_date()
121+
}
122+
req_id, data = sign_request(req, self.identity)
123+
eid = canister_id if effective_canister_id is None else effective_canister_id
124+
_ = await self.call_endpoint_async(eid, req_id, data)
125+
# print('update.req_id:', req_id.hex())
126+
status, result = await self.poll_async(eid, req_id, **kwargs)
127+
if status == 'rejected':
128+
raise Exception('Rejected: ' + result.decode())
129+
elif status == 'replied':
130+
return decode(result, return_type)
131+
else:
132+
raise Exception('Timeout to poll result, current status: ' + str(status))
86133

87134
def read_state_raw(self, canister_id, paths):
88135
req = {
@@ -101,6 +148,23 @@ def read_state_raw(self, canister_id, paths):
101148
cert = cbor2.loads(d['certificate'])
102149
return cert
103150

151+
async def read_state_raw_async(self, canister_id, paths):
152+
req = {
153+
'request_type': 'read_state',
154+
'sender': self.identity.sender().bytes,
155+
'paths': paths,
156+
'ingress_expiry': self.get_expiry_date(),
157+
}
158+
_, data = sign_request(req, self.identity)
159+
ret = await self.read_state_endpoint_async(canister_id, data)
160+
if ret == b'Invalid path requested.':
161+
raise ValueError('Invalid path requested!')
162+
elif ret == b'Could not parse body as read request: invalid type: byte array, expected a sequence':
163+
raise ValueError('Could not parse body as read request: invalid type: byte array, expected a sequence')
164+
d = cbor2.loads(ret)
165+
cert = cbor2.loads(d['certificate'])
166+
return cert
167+
104168
def request_status_raw(self, canister_id, req_id):
105169
paths = [
106170
['request_status'.encode(), req_id],
@@ -112,6 +176,17 @@ def request_status_raw(self, canister_id, req_id):
112176
else:
113177
return status.decode(), cert
114178

179+
async def request_status_raw_async(self, canister_id, req_id):
180+
paths = [
181+
['request_status'.encode(), req_id],
182+
]
183+
cert = await self.read_state_raw_async(canister_id, paths)
184+
status = lookup(['request_status'.encode(), req_id, 'status'.encode()], cert)
185+
if (status == None):
186+
return status, cert
187+
else:
188+
return status.decode(), cert
189+
115190
def poll(self, canister_id, req_id, delay=1, timeout=float('inf')):
116191
status = None
117192
for _ in wait(delay, timeout):
@@ -129,3 +204,21 @@ def poll(self, canister_id, req_id, delay=1, timeout=float('inf')):
129204
return status, msg
130205
else:
131206
return status, _
207+
208+
async def poll_async(self, canister_id, req_id, delay=1, timeout=float('inf')):
209+
status = None
210+
for _ in wait(delay, timeout):
211+
status, cert = await self.request_status_raw_async(canister_id, req_id)
212+
if status == 'replied' or status == 'done' or status == 'rejected':
213+
break
214+
215+
if status == 'replied':
216+
path = ['request_status'.encode(), req_id, 'reply'.encode()]
217+
res = lookup(path, cert)
218+
return status, res
219+
elif status == 'rejected':
220+
path = ['request_status'.encode(), req_id, 'reject_message'.encode()]
221+
msg = lookup(path, cert)
222+
return status, msg
223+
else:
224+
return status, _

ic/canister.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(self, agent, canister_id, candid=None):
3333
assert type(method) == FuncClass
3434
anno = None if len(method.annotations) == 0 else method.annotations[0]
3535
setattr(self, name, CaniterMethod(agent, canister_id, name, method.argTypes, method.retTypes, anno))
36+
setattr(self, name + '_async', CaniterMethodAsync(agent, canister_id, name, method.argTypes, method.retTypes, anno))
3637

3738
class CaniterMethod:
3839
def __init__(self, agent, canister_id, name, args, rets, anno = None):
@@ -73,3 +74,43 @@ def __call__(self, *args, **kwargs):
7374
return res
7475

7576
return list(map(lambda item: item["value"], res))
77+
78+
class CaniterMethodAsync:
79+
def __init__(self, agent, canister_id, name, args, rets, anno = None):
80+
self.agent = agent
81+
self.canister_id = canister_id
82+
self.name = name
83+
self.args = args
84+
self.rets = rets
85+
86+
self.anno = anno
87+
88+
async def __call__(self, *args, **kwargs):
89+
if len(args) != len(self.args):
90+
raise ValueError("Arguments length not match")
91+
arguments = []
92+
for i, arg in enumerate(args):
93+
arguments.append({"type": self.args[i], "value": arg})
94+
95+
effective_cansiter_id = args[0]['canister_id'] if self.canister_id == 'aaaaa-aa' and len(args) > 0 and type(args[0]) == dict and 'canister_id' in args[0] else self.canister_id
96+
if self.anno == 'query':
97+
res = await self.agent.query_raw_async(
98+
self.canister_id,
99+
self.name,
100+
encode(arguments),
101+
self.rets,
102+
effective_cansiter_id
103+
)
104+
else:
105+
res = await self.agent.update_raw_async(
106+
self.canister_id,
107+
self.name,
108+
encode(arguments),
109+
self.rets,
110+
effective_cansiter_id
111+
)
112+
113+
if type(res) is not list:
114+
return res
115+
116+
return list(map(lambda item: item["value"], res))

ic/client.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# http client
22

3-
import requests
3+
import httpx
44

55
class Client:
66
def __init__(self, url = "https://ic0.app"):
@@ -9,23 +9,51 @@ def __init__(self, url = "https://ic0.app"):
99
def query(self, canister_id, data):
1010
endpoint = self.url + '/api/v2/canister/' + canister_id + '/query'
1111
headers = {'Content-Type': 'application/cbor'}
12-
ret = requests.post(endpoint, data, headers=headers)
12+
ret = httpx.post(endpoint, data = data, headers=headers)
1313
return ret.content
1414

1515
def call(self, canister_id, req_id, data):
1616
endpoint = self.url + '/api/v2/canister/' + canister_id + '/call'
1717
headers = {'Content-Type': 'application/cbor'}
18-
ret = requests.post(endpoint, data, headers=headers)
18+
ret = httpx.post(endpoint, data = data, headers=headers)
1919
return req_id
2020

2121
def read_state(self, canister_id, data):
2222
endpoint = self.url + '/api/v2/canister/' + canister_id + '/read_state'
2323
headers = {'Content-Type': 'application/cbor'}
24-
ret = requests.post(endpoint, data, headers=headers)
24+
ret = httpx.post(endpoint, data = data, headers=headers)
2525
return ret.content
2626

2727
def status(self):
2828
endpoint = self.url + '/api/v2/status'
29-
ret = requests.get(endpoint)
29+
ret = httpx.get(endpoint)
3030
print('client.status:', ret.text)
3131
return ret.content
32+
33+
async def query_async(self, canister_id, data):
34+
async with httpx.AsyncClient() as client:
35+
endpoint = self.url + '/api/v2/canister/' + canister_id + '/query'
36+
headers = {'Content-Type': 'application/cbor'}
37+
ret = await client.post(endpoint, data = data, headers=headers)
38+
return ret.content
39+
40+
async def call_async(self, canister_id, req_id, data):
41+
async with httpx.AsyncClient() as client:
42+
endpoint = self.url + '/api/v2/canister/' + canister_id + '/call'
43+
headers = {'Content-Type': 'application/cbor'}
44+
await client.post(endpoint, data = data, headers=headers)
45+
return req_id
46+
47+
async def read_state_async(self, canister_id, data):
48+
async with httpx.AsyncClient() as client:
49+
endpoint = self.url + '/api/v2/canister/' + canister_id + '/read_state'
50+
headers = {'Content-Type': 'application/cbor'}
51+
ret = await client.post(endpoint, data = data, headers=headers)
52+
return ret.content
53+
54+
async def status_async(self):
55+
async with httpx.AsyncClient() as client:
56+
endpoint = self.url + '/api/v2/status'
57+
ret = await client.get(endpoint)
58+
print('client.status:', ret.text)
59+
return ret.content

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
author_email = 'hello@rocklabs.io',
1818
keywords = 'dfinity ic agent',
1919
install_requires = [
20-
'requests>=2.22.0',
20+
'httpx>=0.22.0',
2121
'ecdsa>=0.18.0b2',
2222
'cbor2>=5.4.2',
2323
'leb128>=1.0.4',

test_agent.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from ic.agent import *
23
from ic.identity import *
34
from ic.client import *
@@ -8,6 +9,7 @@
89
print('principal:', Principal.self_authenticating(iden.der_pubkey))
910
ag = Agent(iden, client)
1011

12+
start = time.time()
1113
# query token totalSupply
1214
ret = ag.query_raw("gvbup-jyaaa-aaaah-qcdwa-cai", "totalSupply", encode([]))
1315
print('totalSupply:', ret)
@@ -36,3 +38,38 @@
3638
])
3739
)
3840
print('result: ', ret)
41+
42+
t = time.time()
43+
print("sync call elapsed: ", t - start)
44+
45+
async def test_async():
46+
ret = await ag.query_raw_async("gvbup-jyaaa-aaaah-qcdwa-cai", "totalSupply", encode([]))
47+
print('totalSupply:', ret)
48+
49+
# query token name
50+
ret = await ag.query_raw_async("gvbup-jyaaa-aaaah-qcdwa-cai", "name", encode([]))
51+
print('name:', ret)
52+
53+
# query token balance of user
54+
ret = await ag.query_raw_async(
55+
"gvbup-jyaaa-aaaah-qcdwa-cai",
56+
"balanceOf",
57+
encode([
58+
{'type': Types.Principal, 'value': iden.sender().bytes}
59+
])
60+
)
61+
print('balanceOf:', ret)
62+
63+
# transfer 100 tokens to blackhole
64+
ret = await ag.update_raw_async(
65+
"gvbup-jyaaa-aaaah-qcdwa-cai",
66+
"transfer",
67+
encode([
68+
{'type': Types.Principal, 'value': 'aaaaa-aa'},
69+
{'type': Types.Nat, 'value': 10000000000}
70+
])
71+
)
72+
print('result: ', ret)
73+
74+
asyncio.run(test_async())
75+
print("sync call elapsed: ", time.time() - t)

test_canister.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from ic.canister import Canister
23
from ic.client import Client
34
from ic.identity import Identity
@@ -435,4 +436,18 @@
435436
'include_status': [1]
436437
}
437438
)
438-
print(res)
439+
print(res)
440+
441+
async def async_test():
442+
res = await governance.list_proposals_async(
443+
{
444+
'include_reward_status': [],
445+
'before_proposal': [],
446+
'limit': 100,
447+
'exclude_topic': [],
448+
'include_status': [1]
449+
}
450+
)
451+
print(res)
452+
453+
asyncio.run(async_test())

0 commit comments

Comments
 (0)