gate-io-api 0.0.65__py3-none-any.whl → 0.0.100__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of gate-io-api might be problematic. Click here for more details.
- gate/ccxt/__init__.py +2 -1
- gate/ccxt/abstract/gate.py +62 -18
- gate/ccxt/async_support/__init__.py +2 -1
- gate/ccxt/async_support/base/exchange.py +165 -27
- gate/ccxt/async_support/base/throttler.py +1 -1
- gate/ccxt/async_support/base/ws/client.py +194 -64
- gate/ccxt/async_support/base/ws/future.py +27 -50
- gate/ccxt/async_support/gate.py +356 -253
- gate/ccxt/base/decimal_to_precision.py +14 -10
- gate/ccxt/base/errors.py +6 -0
- gate/ccxt/base/exchange.py +606 -119
- gate/ccxt/base/types.py +4 -0
- gate/ccxt/gate.py +356 -253
- gate/ccxt/pro/__init__.py +2 -89
- gate/ccxt/pro/gate.py +14 -7
- {gate_io_api-0.0.65.dist-info → gate_io_api-0.0.100.dist-info}/METADATA +70 -25
- {gate_io_api-0.0.65.dist-info → gate_io_api-0.0.100.dist-info}/RECORD +18 -19
- gate/ccxt/async_support/base/ws/aiohttp_client.py +0 -147
- {gate_io_api-0.0.65.dist-info → gate_io_api-0.0.100.dist-info}/WHEEL +0 -0
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
orjson = None
|
|
4
|
+
try:
|
|
5
|
+
import orjson as orjson
|
|
6
|
+
except ImportError:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
from asyncio import sleep, ensure_future, wait_for, TimeoutError, BaseEventLoop, Future as asyncioFuture
|
|
12
|
+
from .functions import milliseconds, iso8601, deep_extend, is_json_encoded_object
|
|
13
|
+
from ccxt import NetworkError, RequestTimeout
|
|
6
14
|
from ccxt.async_support.base.ws.future import Future
|
|
7
|
-
from
|
|
15
|
+
from ccxt.async_support.base.ws.functions import gunzip, inflate
|
|
16
|
+
from typing import Dict
|
|
17
|
+
|
|
18
|
+
from aiohttp import WSMsgType
|
|
19
|
+
|
|
8
20
|
|
|
9
21
|
class Client(object):
|
|
10
22
|
|
|
11
23
|
url = None
|
|
12
24
|
ws = None
|
|
13
|
-
futures = {}
|
|
25
|
+
futures: Dict[str, Future] = {}
|
|
14
26
|
options = {} # ws-specific options
|
|
15
27
|
subscriptions = {}
|
|
16
28
|
rejections = {}
|
|
17
|
-
message_queue = {}
|
|
18
|
-
useMessageQueue = False
|
|
19
29
|
on_message_callback = None
|
|
20
30
|
on_error_callback = None
|
|
21
31
|
on_close_callback = None
|
|
@@ -32,14 +42,15 @@ class Client(object):
|
|
|
32
42
|
maxPingPongMisses = 2.0 # how many missed pongs to raise a timeout
|
|
33
43
|
lastPong = None
|
|
34
44
|
ping = None # ping-function if defined
|
|
45
|
+
proxy = None
|
|
35
46
|
verbose = False # verbose output
|
|
36
47
|
gunzip = False
|
|
37
48
|
inflate = False
|
|
38
49
|
throttle = None
|
|
39
50
|
connecting = False
|
|
40
|
-
asyncio_loop = None
|
|
51
|
+
asyncio_loop: BaseEventLoop = None
|
|
41
52
|
ping_looper = None
|
|
42
|
-
|
|
53
|
+
decompressBinary = True # decompress binary messages by default
|
|
43
54
|
|
|
44
55
|
def __init__(self, url, on_message_callback, on_error_callback, on_close_callback, on_connected_callback, config={}):
|
|
45
56
|
defaults = {
|
|
@@ -70,37 +81,25 @@ class Client(object):
|
|
|
70
81
|
if message_hash in self.rejections:
|
|
71
82
|
future.reject(self.rejections[message_hash])
|
|
72
83
|
del self.rejections[message_hash]
|
|
73
|
-
del self.message_queue[message_hash]
|
|
74
|
-
return future
|
|
75
|
-
if self.useMessageQueue and message_hash in self.message_queue:
|
|
76
|
-
queue = self.message_queue[message_hash]
|
|
77
|
-
if len(queue):
|
|
78
|
-
future.resolve(queue.popleft())
|
|
79
|
-
del self.futures[message_hash]
|
|
80
84
|
return future
|
|
81
85
|
|
|
86
|
+
def reusable_future(self, message_hash):
|
|
87
|
+
return self.future(message_hash) # only used in go
|
|
88
|
+
|
|
89
|
+
def reusableFuture(self, message_hash):
|
|
90
|
+
return self.future(message_hash) # only used in go
|
|
91
|
+
|
|
82
92
|
def resolve(self, result, message_hash):
|
|
83
93
|
if self.verbose and message_hash is None:
|
|
84
94
|
self.log(iso8601(milliseconds()), 'resolve received None messageHash')
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
queue = self.message_queue[message_hash]
|
|
90
|
-
queue.append(result)
|
|
91
|
-
if message_hash in self.futures:
|
|
92
|
-
future = self.futures[message_hash]
|
|
93
|
-
future.resolve(queue.popleft())
|
|
94
|
-
del self.futures[message_hash]
|
|
95
|
-
else:
|
|
96
|
-
if message_hash in self.futures:
|
|
97
|
-
future = self.futures[message_hash]
|
|
98
|
-
future.resolve(result)
|
|
99
|
-
del self.futures[message_hash]
|
|
95
|
+
if message_hash in self.futures:
|
|
96
|
+
future = self.futures[message_hash]
|
|
97
|
+
future.resolve(result)
|
|
98
|
+
del self.futures[message_hash]
|
|
100
99
|
return result
|
|
101
100
|
|
|
102
101
|
def reject(self, result, message_hash=None):
|
|
103
|
-
if message_hash:
|
|
102
|
+
if message_hash is not None:
|
|
104
103
|
if message_hash in self.futures:
|
|
105
104
|
future = self.futures[message_hash]
|
|
106
105
|
future.reject(result)
|
|
@@ -113,19 +112,41 @@ class Client(object):
|
|
|
113
112
|
self.reject(result, message_hash)
|
|
114
113
|
return result
|
|
115
114
|
|
|
116
|
-
|
|
115
|
+
def receive_loop(self):
|
|
117
116
|
if self.verbose:
|
|
118
117
|
self.log(iso8601(milliseconds()), 'receive loop')
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
self.
|
|
118
|
+
if not self.closed():
|
|
119
|
+
# let's drain the aiohttp buffer to avoid latency
|
|
120
|
+
if self.buffer and len(self.buffer) > 1:
|
|
121
|
+
size_delta = 0
|
|
122
|
+
while len(self.buffer) > 1:
|
|
123
|
+
message, size = self.buffer.popleft()
|
|
124
|
+
size_delta += size
|
|
125
|
+
self.handle_message(message)
|
|
126
|
+
# we must update the size of the last message inside WebSocketDataQueue
|
|
127
|
+
# self.receive() calls WebSocketDataQueue.read() that calls WebSocketDataQueue._read_from_buffer()
|
|
128
|
+
# which updates the size of the buffer, the _size will overflow and pause the transport
|
|
129
|
+
# make sure to set the enviroment variable AIOHTTP_NO_EXTENSIONS=Y to check
|
|
130
|
+
# print(self.connection._conn.protocol._payload._size)
|
|
131
|
+
self.buffer[0] = (self.buffer[0][0], self.buffer[0][1] + size_delta)
|
|
132
|
+
|
|
133
|
+
task = self.asyncio_loop.create_task(self.receive())
|
|
134
|
+
|
|
135
|
+
def after_interrupt(resolved: asyncioFuture):
|
|
136
|
+
exception = resolved.exception()
|
|
137
|
+
if exception is None:
|
|
138
|
+
self.handle_message(resolved.result())
|
|
139
|
+
self.asyncio_loop.call_soon(self.receive_loop)
|
|
140
|
+
else:
|
|
141
|
+
error = NetworkError(str(exception))
|
|
142
|
+
if self.verbose:
|
|
143
|
+
self.log(iso8601(milliseconds()), 'receive_loop', 'Exception', error)
|
|
144
|
+
self.reject(error)
|
|
145
|
+
|
|
146
|
+
task.add_done_callback(after_interrupt)
|
|
147
|
+
else:
|
|
148
|
+
# connection got terminated after the connection was made and before the receive loop ran
|
|
149
|
+
self.on_close(1006)
|
|
129
150
|
|
|
130
151
|
async def open(self, session, backoff_delay=0):
|
|
131
152
|
# exponential backoff for consequent connections if necessary
|
|
@@ -146,7 +167,7 @@ class Client(object):
|
|
|
146
167
|
self.on_connected_callback(self)
|
|
147
168
|
# run both loops forever
|
|
148
169
|
self.ping_looper = ensure_future(self.ping_loop(), loop=self.asyncio_loop)
|
|
149
|
-
self.
|
|
170
|
+
self.asyncio_loop.call_soon(self.receive_loop)
|
|
150
171
|
except TimeoutError:
|
|
151
172
|
# connection timeout
|
|
152
173
|
error = RequestTimeout('Connection timeout')
|
|
@@ -160,6 +181,23 @@ class Client(object):
|
|
|
160
181
|
self.log(iso8601(milliseconds()), 'NetworkError', error)
|
|
161
182
|
self.on_error(error)
|
|
162
183
|
|
|
184
|
+
@property
|
|
185
|
+
def buffer(self):
|
|
186
|
+
# looks like they exposed it in C
|
|
187
|
+
# this means we can bypass it
|
|
188
|
+
# https://github.com/aio-libs/aiohttp/blob/master/aiohttp/_websocket/reader_c.pxd#L53C24-L53C31
|
|
189
|
+
# these checks are necessary to protect these errors: AttributeError: 'NoneType' object has no attribute '_buffer'
|
|
190
|
+
# upon getting an error message
|
|
191
|
+
if self.connection is None:
|
|
192
|
+
return None
|
|
193
|
+
if self.connection._conn is None:
|
|
194
|
+
return None
|
|
195
|
+
if self.connection._conn.protocol is None:
|
|
196
|
+
return None
|
|
197
|
+
if self.connection._conn.protocol._payload is None:
|
|
198
|
+
return None
|
|
199
|
+
return self.connection._conn.protocol._payload._buffer
|
|
200
|
+
|
|
163
201
|
def connect(self, session, backoff_delay=0):
|
|
164
202
|
if not self.connection and not self.connecting:
|
|
165
203
|
self.connecting = True
|
|
@@ -170,7 +208,7 @@ class Client(object):
|
|
|
170
208
|
if self.verbose:
|
|
171
209
|
self.log(iso8601(milliseconds()), 'on_error', error)
|
|
172
210
|
self.error = error
|
|
173
|
-
self.
|
|
211
|
+
self.reject(error)
|
|
174
212
|
self.on_error_callback(self, error)
|
|
175
213
|
if not self.closed():
|
|
176
214
|
ensure_future(self.close(1006), loop=self.asyncio_loop)
|
|
@@ -179,36 +217,128 @@ class Client(object):
|
|
|
179
217
|
if self.verbose:
|
|
180
218
|
self.log(iso8601(milliseconds()), 'on_close', code)
|
|
181
219
|
if not self.error:
|
|
182
|
-
self.
|
|
220
|
+
self.reject(NetworkError('Connection closed by remote server, closing code ' + str(code)))
|
|
183
221
|
self.on_close_callback(self, code)
|
|
184
|
-
|
|
185
|
-
ensure_future(self.close(code), loop=self.asyncio_loop)
|
|
222
|
+
ensure_future(self.aiohttp_close(), loop=self.asyncio_loop)
|
|
186
223
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
self.reject(error)
|
|
224
|
+
def log(self, *args):
|
|
225
|
+
print(*args)
|
|
190
226
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
self.log(iso8601(milliseconds()), 'ping loop')
|
|
227
|
+
def closed(self):
|
|
228
|
+
return (self.connection is None) or self.connection.closed
|
|
194
229
|
|
|
195
230
|
def receive(self):
|
|
196
|
-
|
|
231
|
+
return self.connection.receive()
|
|
232
|
+
|
|
233
|
+
# helper method for binary and text messages
|
|
234
|
+
def handle_text_or_binary_message(self, data):
|
|
235
|
+
if self.verbose:
|
|
236
|
+
self.log(iso8601(milliseconds()), 'message', data)
|
|
237
|
+
if isinstance(data, bytes):
|
|
238
|
+
if self.decompressBinary:
|
|
239
|
+
data = data.decode()
|
|
240
|
+
# decoded = json.loads(data) if is_json_encoded_object(data) else data
|
|
241
|
+
decode = None
|
|
242
|
+
if is_json_encoded_object(data):
|
|
243
|
+
if orjson is None:
|
|
244
|
+
decode = json.loads(data)
|
|
245
|
+
else:
|
|
246
|
+
decode = orjson.loads(data)
|
|
247
|
+
else:
|
|
248
|
+
decode = data
|
|
249
|
+
self.on_message_callback(self, decode)
|
|
197
250
|
|
|
198
251
|
def handle_message(self, message):
|
|
199
|
-
|
|
252
|
+
# self.log(iso8601(milliseconds()), message)
|
|
253
|
+
if message.type == WSMsgType.TEXT:
|
|
254
|
+
self.handle_text_or_binary_message(message.data)
|
|
255
|
+
elif message.type == WSMsgType.BINARY:
|
|
256
|
+
data = message.data
|
|
257
|
+
if self.gunzip:
|
|
258
|
+
data = gunzip(data)
|
|
259
|
+
elif self.inflate:
|
|
260
|
+
data = inflate(data)
|
|
261
|
+
self.handle_text_or_binary_message(data)
|
|
262
|
+
# autoping is responsible for automatically replying with pong
|
|
263
|
+
# to a ping incoming from a server, we have to disable autoping
|
|
264
|
+
# with aiohttp's websockets and respond with pong manually
|
|
265
|
+
# otherwise aiohttp's websockets client won't trigger WSMsgType.PONG
|
|
266
|
+
elif message.type == WSMsgType.PING:
|
|
267
|
+
if self.verbose:
|
|
268
|
+
self.log(iso8601(milliseconds()), 'ping', message)
|
|
269
|
+
ensure_future(self.connection.pong(message.data), loop=self.asyncio_loop)
|
|
270
|
+
elif message.type == WSMsgType.PONG:
|
|
271
|
+
self.lastPong = milliseconds()
|
|
272
|
+
if self.verbose:
|
|
273
|
+
self.log(iso8601(milliseconds()), 'pong', message)
|
|
274
|
+
pass
|
|
275
|
+
elif message.type == WSMsgType.CLOSE:
|
|
276
|
+
if self.verbose:
|
|
277
|
+
self.log(iso8601(milliseconds()), 'close', self.closed(), message)
|
|
278
|
+
self.on_close(message.data)
|
|
279
|
+
elif message.type == WSMsgType.ERROR:
|
|
280
|
+
if self.verbose:
|
|
281
|
+
self.log(iso8601(milliseconds()), 'error', message)
|
|
282
|
+
error = NetworkError(str(message))
|
|
283
|
+
self.on_error(error)
|
|
200
284
|
|
|
201
|
-
def
|
|
202
|
-
|
|
285
|
+
def create_connection(self, session):
|
|
286
|
+
# autoping is responsible for automatically replying with pong
|
|
287
|
+
# to a ping incoming from a server, we have to disable autoping
|
|
288
|
+
# with aiohttp's websockets and respond with pong manually
|
|
289
|
+
# otherwise aiohttp's websockets client won't trigger WSMsgType.PONG
|
|
290
|
+
# call aenter here to simulate async with otherwise we get the error "await not called with future"
|
|
291
|
+
# if connecting to a non-existent endpoint
|
|
292
|
+
if (self.proxy):
|
|
293
|
+
return session.ws_connect(self.url, autoping=False, autoclose=False, headers=self.options.get('headers'), proxy=self.proxy, max_msg_size=10485760).__aenter__()
|
|
294
|
+
return session.ws_connect(self.url, autoping=False, autoclose=False, headers=self.options.get('headers'), max_msg_size=10485760).__aenter__()
|
|
203
295
|
|
|
204
296
|
async def send(self, message):
|
|
205
|
-
|
|
297
|
+
if self.verbose:
|
|
298
|
+
self.log(iso8601(milliseconds()), 'sending', message)
|
|
299
|
+
send_msg = None
|
|
300
|
+
if isinstance(message, str):
|
|
301
|
+
send_msg = message
|
|
302
|
+
else:
|
|
303
|
+
if orjson is None:
|
|
304
|
+
send_msg = json.dumps(message, separators=(',', ':'))
|
|
305
|
+
else:
|
|
306
|
+
send_msg = orjson.dumps(message).decode('utf-8')
|
|
307
|
+
return await self.connection.send_str(send_msg)
|
|
206
308
|
|
|
207
309
|
async def close(self, code=1000):
|
|
208
|
-
|
|
310
|
+
if self.verbose:
|
|
311
|
+
self.log(iso8601(milliseconds()), 'closing', code)
|
|
312
|
+
for future in self.futures.values():
|
|
313
|
+
future.cancel()
|
|
314
|
+
await self.aiohttp_close()
|
|
209
315
|
|
|
210
|
-
def
|
|
211
|
-
|
|
316
|
+
async def aiohttp_close(self):
|
|
317
|
+
if not self.closed():
|
|
318
|
+
await self.connection.close()
|
|
319
|
+
# these will end automatically once self.closed() = True
|
|
320
|
+
# so we don't need to cancel them
|
|
321
|
+
if self.ping_looper:
|
|
322
|
+
self.ping_looper.cancel()
|
|
212
323
|
|
|
213
|
-
def
|
|
214
|
-
|
|
324
|
+
async def ping_loop(self):
|
|
325
|
+
if self.verbose:
|
|
326
|
+
self.log(iso8601(milliseconds()), 'ping loop')
|
|
327
|
+
while self.keepAlive and not self.closed():
|
|
328
|
+
now = milliseconds()
|
|
329
|
+
self.lastPong = now if self.lastPong is None else self.lastPong
|
|
330
|
+
if (self.lastPong + self.keepAlive * self.maxPingPongMisses) < now:
|
|
331
|
+
self.on_error(RequestTimeout('Connection to ' + self.url + ' timed out due to a ping-pong keepalive missing on time'))
|
|
332
|
+
# the following ping-clause is not necessary with aiohttp's built-in ws
|
|
333
|
+
# since it has a heartbeat option (see create_connection above)
|
|
334
|
+
# however some exchanges require a text-type ping message
|
|
335
|
+
# therefore we need this clause anyway
|
|
336
|
+
else:
|
|
337
|
+
if self.ping:
|
|
338
|
+
try:
|
|
339
|
+
await self.send(self.ping(self))
|
|
340
|
+
except Exception as e:
|
|
341
|
+
self.on_error(e)
|
|
342
|
+
else:
|
|
343
|
+
await self.connection.ping()
|
|
344
|
+
await sleep(self.keepAlive / 1000)
|
|
@@ -1,69 +1,46 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from ccxt import ExchangeClosedByUser
|
|
3
2
|
|
|
3
|
+
# Test by running:
|
|
4
|
+
# - python python/ccxt/pro/test/base/test_close.py
|
|
5
|
+
# - python python/ccxt/pro/test/base/test_future.py
|
|
4
6
|
class Future(asyncio.Future):
|
|
5
7
|
|
|
6
|
-
is_race_future = False
|
|
7
|
-
|
|
8
8
|
def resolve(self, result=None):
|
|
9
9
|
if not self.done():
|
|
10
|
-
|
|
11
|
-
self.set_result(result)
|
|
12
|
-
except BaseException as e:
|
|
13
|
-
print("Error in Future.resolve")
|
|
14
|
-
raise e
|
|
10
|
+
self.set_result(result)
|
|
15
11
|
|
|
16
12
|
def reject(self, error=None):
|
|
17
13
|
if not self.done():
|
|
18
|
-
|
|
19
|
-
if not isinstance(error, BaseException):
|
|
20
|
-
error = Exception(error)
|
|
21
|
-
try:
|
|
22
|
-
self.set_exception(error)
|
|
23
|
-
except BaseException as e:
|
|
24
|
-
print("Error in Future.reject")
|
|
25
|
-
raise e
|
|
14
|
+
self.set_exception(error)
|
|
26
15
|
|
|
27
16
|
@classmethod
|
|
28
17
|
def race(cls, futures):
|
|
29
18
|
future = Future()
|
|
30
|
-
for f in futures:
|
|
31
|
-
f.is_race_future = True
|
|
32
19
|
coro = asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED)
|
|
33
20
|
task = asyncio.create_task(coro)
|
|
34
21
|
|
|
35
22
|
def callback(done):
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
first = futures_list[0]
|
|
61
|
-
|
|
62
|
-
first_result = first.result()
|
|
63
|
-
future.resolve(first_result)
|
|
64
|
-
except asyncio.CancelledError as e:
|
|
65
|
-
future.reject(e)
|
|
66
|
-
except Exception as e:
|
|
67
|
-
future.reject(e)
|
|
23
|
+
complete, _ = done.result()
|
|
24
|
+
# check for exceptions
|
|
25
|
+
exceptions = []
|
|
26
|
+
cancelled = False
|
|
27
|
+
for f in complete:
|
|
28
|
+
if f.cancelled():
|
|
29
|
+
cancelled = True
|
|
30
|
+
else:
|
|
31
|
+
err = f.exception()
|
|
32
|
+
if err:
|
|
33
|
+
exceptions.append(err)
|
|
34
|
+
# if any exceptions return with first exception
|
|
35
|
+
if future.cancelled():
|
|
36
|
+
return
|
|
37
|
+
if len(exceptions) > 0:
|
|
38
|
+
future.set_exception(exceptions[0])
|
|
39
|
+
# else return first result
|
|
40
|
+
elif cancelled:
|
|
41
|
+
future.cancel()
|
|
42
|
+
else:
|
|
43
|
+
first_result = list(complete)[0].result()
|
|
44
|
+
future.set_result(first_result)
|
|
68
45
|
task.add_done_callback(callback)
|
|
69
46
|
return future
|