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.

@@ -1,21 +1,31 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- from asyncio import sleep, ensure_future, wait_for, TimeoutError
4
- from .functions import milliseconds, iso8601, deep_extend
5
- from ccxt import NetworkError, RequestTimeout, NotSupported
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 collections import deque
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
- receive_looper = None
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
- if self.useMessageQueue:
87
- if message_hash not in self.message_queue:
88
- self.message_queue[message_hash] = deque(maxlen=10)
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
- async def receive_loop(self):
115
+ def receive_loop(self):
117
116
  if self.verbose:
118
117
  self.log(iso8601(milliseconds()), 'receive loop')
119
- while not self.closed():
120
- try:
121
- message = await self.receive()
122
- # self.log(iso8601(milliseconds()), 'received', message)
123
- self.handle_message(message)
124
- except Exception as e:
125
- error = NetworkError(str(e))
126
- if self.verbose:
127
- self.log(iso8601(milliseconds()), 'receive_loop', 'Exception', error)
128
- self.reset(error)
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.receive_looper = ensure_future(self.receive_loop(), loop=self.asyncio_loop)
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.reset(error)
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.reset(NetworkError('Connection closed by remote server, closing code ' + str(code)))
220
+ self.reject(NetworkError('Connection closed by remote server, closing code ' + str(code)))
183
221
  self.on_close_callback(self, code)
184
- if not self.closed():
185
- ensure_future(self.close(code), loop=self.asyncio_loop)
222
+ ensure_future(self.aiohttp_close(), loop=self.asyncio_loop)
186
223
 
187
- def reset(self, error):
188
- self.message_queue = {}
189
- self.reject(error)
224
+ def log(self, *args):
225
+ print(*args)
190
226
 
191
- async def ping_loop(self):
192
- if self.verbose:
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
- raise NotSupported('receive() not implemented')
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
- raise NotSupported('handle_message() not implemented')
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 closed(self):
202
- raise NotSupported('closed() not implemented')
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
- raise NotSupported('send() not implemented')
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
- raise NotSupported('close() not implemented')
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 create_connection(self, session):
211
- raise NotSupported('create_connection() not implemented')
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 log(self, *args):
214
- print(*args)
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
- try:
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
- # If not an exception, wrap it in a generic Exception
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
- try:
37
- complete, pending = done.result()
38
- # check for exceptions
39
- for i, f in enumerate(complete):
40
- try:
41
- f.result()
42
- except ExchangeClosedByUser as e:
43
- if len(pending) == 0 and i == len(complete) - 1:
44
- future.reject(e)
45
- # wait for all the sub promises to be reject before rejecting future
46
- continue
47
- except asyncio.CancelledError as e:
48
- continue
49
- except Exception as e:
50
- future.reject(e)
51
- return
52
- # no exceptions return first result
53
- futures_list = list(complete)
54
-
55
- are_all_canceled = all([f.cancelled() for f in futures_list])
56
- if are_all_canceled:
57
- future.reject(ExchangeClosedByUser('Connection closed by the user'))
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