exchanges-wrapper 2.1.42__tar.gz → 2.1.45__tar.gz

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.
Files changed (20) hide show
  1. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/PKG-INFO +4 -4
  2. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/README.md +1 -1
  3. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/__init__.py +1 -1
  4. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/client.py +43 -36
  5. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/exch_srv.py +38 -39
  6. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/parsers/okx.py +1 -0
  7. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/web_sockets.py +4 -3
  8. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/pyproject.toml +2 -2
  9. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/LICENSE.md +0 -0
  10. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/definitions.py +0 -0
  11. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/errors.py +0 -0
  12. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/events.py +0 -0
  13. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  14. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/http_client.py +0 -0
  15. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/lib.py +0 -0
  16. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/martin/__init__.py +0 -0
  17. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  18. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/parsers/bybit.py +0 -0
  19. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/parsers/huobi.py +0 -0
  20. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.45}/exchanges_wrapper/proto/martin.proto +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.42
3
+ Version: 2.1.45
4
4
  Summary: REST API and WebSocket asyncio wrapper with grpc powered multiplexer server
5
5
  Author-email: Thomas Marchand <thomas.marchand@tuta.io>, Jerry Fedorenko <jerry.fedorenko@yahoo.com>
6
6
  Requires-Python: >=3.10
@@ -12,10 +12,10 @@ Classifier: Operating System :: Unix
12
12
  Classifier: Operating System :: Microsoft :: Windows
13
13
  Classifier: Operating System :: MacOS
14
14
  License-File: LICENSE.md
15
- Requires-Dist: crypto-ws-api==2.1.3
15
+ Requires-Dist: crypto-ws-api==2.1.5
16
16
  Requires-Dist: pyotp==2.9.0
17
17
  Requires-Dist: simplejson==3.20.2
18
- Requires-Dist: aiohttp~=3.13.0
18
+ Requires-Dist: aiohttp~=3.13.2
19
19
  Requires-Dist: expiringdict~=1.2.2
20
20
  Requires-Dist: betterproto==2.0.0b7
21
21
  Requires-Dist: grpclib~=0.4.8
@@ -157,7 +157,7 @@ docker run -itP \
157
157
  *USDT* (TRC20) TN8F3Dz8BU8VwECRh3LTKi7FrsU8eWfsZz
158
158
 
159
159
  ## Powered by exchanges-wrapper
160
- <a><img align="middle" src="https://github.com/DogsTailFarmer/martin-binance/raw/public/doc/Modified%20martingale.svg" width="50"></a>
160
+ <a><img align="middle" src="https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/55758a8d32b08c4deb1b34add01c5e259b88f738/doc/Modified%20martingale.svg" width="50"></a>
161
161
  [martin-binance](https://github.com/DogsTailFarmer/martin-binance)
162
162
 
163
163
  Free trading system for crypto exchanges SPOT markets. Adaptive customizable reverse grid strategy based on martingale.
@@ -134,7 +134,7 @@ docker run -itP \
134
134
  *USDT* (TRC20) TN8F3Dz8BU8VwECRh3LTKi7FrsU8eWfsZz
135
135
 
136
136
  ## Powered by exchanges-wrapper
137
- <a><img align="middle" src="https://github.com/DogsTailFarmer/martin-binance/raw/public/doc/Modified%20martingale.svg" width="50"></a>
137
+ <a><img align="middle" src="https://raw.githubusercontent.com/DogsTailFarmer/martin-binance/55758a8d32b08c4deb1b34add01c5e259b88f738/doc/Modified%20martingale.svg" width="50"></a>
138
138
  [martin-binance](https://github.com/DogsTailFarmer/martin-binance)
139
139
 
140
140
  Free trading system for crypto exchanges SPOT markets. Adaptive customizable reverse grid strategy based on martingale.
@@ -12,7 +12,7 @@ __maintainer__ = "Jerry Fedorenko"
12
12
  __contact__ = "https://github.com/DogsTailFarmer"
13
13
  __email__ = "jerry.fedorenko@yahoo.com"
14
14
  __credits__ = ["https://github.com/DanyaSWorlD"]
15
- __version__ = "2.1.42"
15
+ __version__ = "2.1.45"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -51,7 +51,7 @@ class Client:
51
51
  'passphrase', 'endpoint_api_public', 'endpoint_ws_public',
52
52
  'endpoint_api_auth', 'endpoint_ws_auth', 'endpoint_ws_api',
53
53
  'ws_add_on', 'master_email', 'master_name', 'two_fa', 'http',
54
- 'user_session', 'loaded', 'symbols', 'highest_precision',
54
+ 'user_session', 'symbols', 'highest_precision',
55
55
  'rate_limits', 'data_streams', 'active_orders', 'wss_buffer',
56
56
  'stream_queue', 'on_order_update_queues', 'account_id',
57
57
  'account_uid', 'main_account_id', 'main_account_uid',
@@ -98,7 +98,6 @@ class Client:
98
98
  else:
99
99
  self.user_session = None
100
100
  #
101
- self.loaded = False
102
101
  self.symbols = {}
103
102
  self.highest_precision = None
104
103
  self.rate_limits = None
@@ -150,7 +149,6 @@ class Client:
150
149
  logger.info(f"Main ByBit account UID: {self.main_account_uid}, sub-UID: {self.account_uid}")
151
150
  # load rate limits
152
151
  self.rate_limits = infos["rateLimits"]
153
- self.loaded = True
154
152
  logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
155
153
 
156
154
  async def close(self):
@@ -229,7 +227,7 @@ class Client:
229
227
  await self.user_session.stop(_trade_id)
230
228
 
231
229
  def assert_symbol_exists(self, symbol):
232
- if self.loaded and symbol not in self.symbols:
230
+ if symbol not in self.symbols:
233
231
  raise ExchangePyError(f"Symbol {symbol} is not valid according to the loaded exchange infos")
234
232
 
235
233
  def symbol_to_bfx(self, symbol) -> str:
@@ -246,6 +244,9 @@ class Client:
246
244
  symbol_info = self.symbols.get(symbol)
247
245
  return f"{symbol_info.get('baseAsset')}-{symbol_info.get('quoteAsset')}"
248
246
 
247
+ def symbol_to_id(self, symbol) -> int:
248
+ return self.symbols.get(symbol).get('instIdCode')
249
+
249
250
  def active_order(self, order_id: int, quantity="0", executed_qty="0", last_event=None):
250
251
  quantity_decimal = Decimal(quantity)
251
252
  executed_qty_decimal = Decimal(executed_qty)
@@ -279,32 +280,31 @@ class Client:
279
280
  def refine_amount(self, symbol, amount: Union[str, Decimal], _quote=False):
280
281
  if type(amount) is str: # to save time for developers
281
282
  amount = Decimal(amount)
282
- if self.loaded:
283
- precision = self.symbols[symbol]["baseAssetPrecision"]
284
- lot_size_filter = self.symbols[symbol]["filters"]["LOT_SIZE"]
285
- step_size = Decimal(lot_size_filter["stepSize"])
286
- # noinspection PyStringFormat
287
- amount = (
288
- (f"%.{precision}f" % truncate(amount if _quote else (amount - amount % step_size), precision))
289
- .rstrip("0")
290
- .rstrip(".")
291
- )
283
+
284
+ precision = self.symbols[symbol]["baseAssetPrecision"]
285
+ lot_size_filter = self.symbols[symbol]["filters"]["LOT_SIZE"]
286
+ step_size = Decimal(lot_size_filter["stepSize"])
287
+ # noinspection PyStringFormat
288
+ amount = (
289
+ (f"%.{precision}f" % truncate(amount if _quote else (amount - amount % step_size), precision))
290
+ .rstrip("0")
291
+ .rstrip(".")
292
+ )
292
293
  return amount
293
294
 
294
295
  def refine_price(self, symbol, price: Union[str, Decimal]):
295
296
  if isinstance(price, str): # to save time for developers
296
297
  price = Decimal(price)
297
298
 
298
- if self.loaded:
299
- precision = self.symbols[symbol]["baseAssetPrecision"]
300
- price_filter = self.symbols[symbol]["filters"]["PRICE_FILTER"]
301
- price = price - (price % Decimal(price_filter["tickSize"]))
302
- # noinspection PyStringFormat
303
- price = (
304
- (f"%.{precision}f" % truncate(price, precision))
305
- .rstrip("0")
306
- .rstrip(".")
307
- )
299
+ precision = self.symbols[symbol]["baseAssetPrecision"]
300
+ price_filter = self.symbols[symbol]["filters"]["PRICE_FILTER"]
301
+ price = price - (price % Decimal(price_filter["tickSize"]))
302
+ # noinspection PyStringFormat
303
+ price = (
304
+ (f"%.{precision}f" % truncate(price, precision))
305
+ .rstrip("0")
306
+ .rstrip(".")
307
+ )
308
308
  return price
309
309
 
310
310
  def assert_symbol(self, symbol):
@@ -861,7 +861,7 @@ class Client:
861
861
  self.active_order(int(res), quantity, binance_res['executedQty'])
862
862
  elif self.exchange == 'okx':
863
863
  params = {
864
- "instId": self.symbol_to_okx(symbol),
864
+ "instIdCode": self.symbol_to_id(symbol),
865
865
  "tdMode": "cash",
866
866
  "clOrdId": new_client_order_id,
867
867
  "side": side.lower(),
@@ -870,6 +870,7 @@ class Client:
870
870
  "px": price,
871
871
  }
872
872
  res = await self.user_session.handle_request(trade_id, "order", _params=params)
873
+ params["instId"] = self.symbol_to_okx(symbol)
873
874
  if res is None:
874
875
  fallback_warning(self.exchange, symbol)
875
876
  res = await self.http.send_api_call(
@@ -1074,19 +1075,19 @@ class Client:
1074
1075
  _queue = asyncio.Queue()
1075
1076
  self.on_order_update_queues.update({f"{_symbol}{order_id}": _queue})
1076
1077
  params = {
1077
- "instId": _symbol,
1078
+ "instIdCode": self.symbol_to_id(symbol),
1078
1079
  "ordId": str(order_id),
1079
1080
  "clOrdId": str(origin_client_order_id),
1080
1081
  }
1081
- _res = (
1082
- await self.user_session.handle_request(trade_id, "cancel-order", _params=params)
1083
- or await self.http.send_api_call(
1084
- "/api/v5/trade/cancel-order",
1085
- method="POST",
1086
- signed=True,
1087
- **params,
1088
- )
1089
- )
1082
+ _res = await self.user_session.handle_request(trade_id, "cancel-order", _params=params)
1083
+ if _res is None:
1084
+ params["instId"] = self.symbol_to_okx(symbol)
1085
+ _res = await self.http.send_api_call(
1086
+ "/api/v5/trade/cancel-order",
1087
+ method="POST",
1088
+ signed=True,
1089
+ **params,
1090
+ )
1090
1091
  if _res[0].get('sCode') != '0':
1091
1092
  raise UserWarning(_res[0].get('sMsg'))
1092
1093
  try:
@@ -1183,7 +1184,13 @@ class Client:
1183
1184
  for order in orders:
1184
1185
  order['status'] = 'CANCELED'
1185
1186
  orders_canceled.append(order)
1186
- params.append({'instId': _symbol, 'ordId': order.get('orderId')})
1187
+ params.append(
1188
+ {
1189
+ "instIdCode": self.symbol_to_id(symbol),
1190
+ 'instId': _symbol,
1191
+ 'ordId': order.get('orderId')
1192
+ }
1193
+ )
1187
1194
  if i >= 19:
1188
1195
  break
1189
1196
  i += 1
@@ -8,6 +8,8 @@ import grpclib.exceptions
8
8
  from exchanges_wrapper import __version__ as VER_EW
9
9
  # noinspection PyPep8Naming
10
10
  from crypto_ws_api import __version__ as VER_CW
11
+ from crypto_ws_api.ws_session import set_logger
12
+
11
13
  import time
12
14
  import weakref
13
15
  import gc
@@ -16,8 +18,9 @@ import asyncio
16
18
  import functools
17
19
  # noinspection PyPackageRequirements
18
20
  import ujson as json
19
- import logging.handlers
21
+ import logging
20
22
  from decimal import Decimal
23
+ import ctypes, ctypes.util
21
24
 
22
25
  import exchanges_wrapper.martin as mr
23
26
  from exchanges_wrapper import WORK_PATH, LOG_FILE, errors, Server, Status, GRPCError, graceful_exit
@@ -41,24 +44,12 @@ HEARTBEAT = 1 # sec
41
44
  MAX_QUEUE_SIZE = 100
42
45
  WSS_TICKER_TIMEOUT = 600 # sec
43
46
  #
44
- logger = logging.getLogger(__name__)
45
- formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
46
- #
47
- fh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1000000, backupCount=10)
48
- fh.setFormatter(formatter)
49
- fh.setLevel(logging.DEBUG)
50
- #
51
- sh = logging.StreamHandler()
52
- sh.setFormatter(formatter)
53
- sh.setLevel(logging.INFO)
54
- #
55
- root_logger = logging.getLogger()
56
- root_logger.setLevel(min([fh.level, sh.level]))
57
- root_logger.addHandler(fh)
58
- root_logger.addHandler(sh)
59
-
60
- logging.basicConfig()
47
+ logger = set_logger(__name__, LOG_FILE, file_level=logging.DEBUG, set_root_logger=True)
61
48
  logging.getLogger('hpack').setLevel(logging.INFO)
49
+ logging.getLogger('grpclib.protocol').setLevel(logging.INFO)
50
+
51
+ def malloc_trim(trim_type: int = 0):
52
+ ctypes.CDLL(ctypes.util.find_library('c')).malloc_trim(trim_type)
62
53
 
63
54
 
64
55
  class OpenClient:
@@ -94,9 +85,9 @@ class OpenClient:
94
85
  return res
95
86
 
96
87
  @classmethod
97
- def remove_client(cls, _account_name):
98
- cls.open_clients[:] = [i for i in cls.open_clients if i.name != _account_name]
99
-
88
+ def remove_client(cls, _id):
89
+ # noinspection PyTypeHints
90
+ cls.open_clients[:] = [i for i in cls.open_clients if id(i) != _id]
100
91
 
101
92
  # noinspection PyPep8Naming,PyMethodMayBeStatic
102
93
  class Martin(mr.MartinBase):
@@ -144,7 +135,7 @@ class Martin(mr.MartinBase):
144
135
  await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
145
136
  except asyncio.exceptions.TimeoutError:
146
137
  await OpenClient.get_client(client_id).client.http.close_session()
147
- OpenClient.remove_client(request.account_name)
138
+ OpenClient.remove_client(client_id)
148
139
  raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
149
140
  except Exception as ex:
150
141
  logger.warning(f"OpenClientConnection for '{open_client.name}' exception: {ex}")
@@ -258,7 +249,7 @@ class Martin(mr.MartinBase):
258
249
 
259
250
  async def fetch_order(self, request: mr.FetchOrderRequest) -> mr.FetchOrderResponse:
260
251
  response = mr.FetchOrderResponse()
261
- res, _, msg_header = await self.send_request(
252
+ res, client, msg_header = await self.send_request(
262
253
  'fetch_order',
263
254
  request,
264
255
  rate_limit=True,
@@ -269,14 +260,15 @@ class Martin(mr.MartinBase):
269
260
  )
270
261
  logger.debug(f"{msg_header}: {res}")
271
262
 
272
- if res and request.filled_update_call and Decimal(res['executedQty']):
273
- request.order_id = res['orderId']
274
- await self.create_trade_stream_event(request, res)
263
+ if _queue := client.on_order_update_queues.get(request.trade_id):
264
+ if res and request.filled_update_call and Decimal(res['executedQty']):
265
+ request.order_id = res['orderId']
266
+ await self.create_trade_stream_event(request, res, _queue)
275
267
  response.from_pydict(res)
276
268
  return response
277
269
 
278
- async def create_trade_stream_event(self, request, order):
279
- trades, client, msg_header = await self.send_request(
270
+ async def create_trade_stream_event(self, request, order, _queue):
271
+ trades, _, msg_header = await self.send_request(
280
272
  'fetch_order_trade_list',
281
273
  request,
282
274
  trade_id=request.trade_id,
@@ -284,8 +276,6 @@ class Martin(mr.MartinBase):
284
276
  order_id=request.order_id
285
277
  )
286
278
 
287
- _queue = client.on_order_update_queues.get(request.trade_id)
288
-
289
279
  for trade in trades:
290
280
  trade['updateTime'] = trade.pop('time')
291
281
  trade |= {
@@ -529,10 +519,12 @@ class Martin(mr.MartinBase):
529
519
  _event_type = f"{_symbol}@miniTicker"
530
520
  client.events.register_event(functools.partial(event_handler, _queue, client, request.trade_id, _event_type),
531
521
  _event_type, client.exchange, request.trade_id)
522
+ Martin.ticker_update_time[request.trade_id] = time.time()
532
523
  while True:
533
524
  _event = await _queue.get()
534
525
  if isinstance(_event, str) and _event == request.trade_id:
535
526
  client.stream_queue.get(request.trade_id, set()).discard(_queue)
527
+ Martin.ticker_update_time.pop(request.trade_id, None)
536
528
  logger.info(f"OnTickerUpdate: Stop loop for {open_client.name}: {request.symbol}")
537
529
  return
538
530
  else:
@@ -774,19 +766,23 @@ class Martin(mr.MartinBase):
774
766
  return response
775
767
 
776
768
  async def check_stream(self, request: mr.MarketRequest) -> mr.SimpleResponse:
777
- response = mr.SimpleResponse()
778
- check_time = time.time() - Martin.ticker_update_time.get(request.trade_id, time.time() - WSS_TICKER_TIMEOUT - 1)
779
- if check_time < WSS_TICKER_TIMEOUT:
780
- response.success = True
781
- else:
782
- response.success = False
769
+ last_update = Martin.ticker_update_time.get(request.trade_id, 0)
770
+ check_time = time.time() - last_update
771
+ success = check_time < WSS_TICKER_TIMEOUT
772
+ response = mr.SimpleResponse(success=success)
773
+ if not success:
774
+ Martin.ticker_update_time.pop(request.trade_id, None)
783
775
  logger.warning(f"CheckStream request failed for {request.trade_id}")
784
776
  return response
785
777
 
786
778
  async def client_restart(self, request: mr.MarketRequest) -> mr.SimpleResponse:
787
- if session := OpenClient.get_client(request.client_id).client.http:
788
- await session.close_session()
789
- OpenClient.remove_client(request.account_name)
779
+ await self.stop_stream(request)
780
+ if client := OpenClient.get_client(request.client_id).client:
781
+ if user_session := client.user_session:
782
+ await user_session.stop(request.trade_id)
783
+ if session := client.http:
784
+ await session.close_session()
785
+ OpenClient.remove_client(request.client_id)
790
786
  return mr.SimpleResponse(success=True)
791
787
 
792
788
 
@@ -797,7 +793,9 @@ async def stop_stream_ex(client, trade_id):
797
793
  await asyncio.sleep(0)
798
794
  client.on_order_update_queues.pop(trade_id, None)
799
795
  client.stream_queue.pop(trade_id, None)
796
+ Martin.ticker_update_time.pop(trade_id, None)
800
797
  gc.collect(generation=2)
798
+ malloc_trim()
801
799
 
802
800
 
803
801
  async def event_handler(_queue, client, trade_id, _event_type, event):
@@ -841,5 +839,6 @@ def main():
841
839
  print(f"Exception: {expt}")
842
840
  print(traceback.format_exc())
843
841
 
842
+
844
843
  if __name__ == '__main__':
845
844
  main()
@@ -78,6 +78,7 @@ def exchange_info(server_time: int, trading_symbol: list, tickers: list, symbol_
78
78
  "isMarginTradingAllowed": False,
79
79
  "filters": [_price_filter, _lot_size, _min_notional, _percent_price],
80
80
  "permissions": ["SPOT"],
81
+ "instIdCode": market.get("instIdCode")
81
82
  }
82
83
  symbols.append(symbol)
83
84
 
@@ -84,7 +84,7 @@ class EventsDataStream:
84
84
  except ConnectionClosed as ex:
85
85
  self.websocket = None
86
86
  ct = str(datetime.now(timezone.utc).replace(tzinfo=None) - start_time).rsplit('.')[0]
87
- self.logger.info(f"WSS life time for {self.exchange} is {ct}")
87
+ self.logger.info(f"WSS life time for {self.exchange}:{self.trade_id} is {ct}")
88
88
  if ex.rcvd and ex.rcvd.code == 4000:
89
89
  self.logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
90
90
  break
@@ -262,7 +262,7 @@ class EventsDataStream:
262
262
  await asyncio.sleep(interval)
263
263
  try:
264
264
  await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
265
- except (ConnectionClosed, asyncio.exceptions.TimeoutError):
265
+ except (ConnectionClosed, asyncio.exceptions.TimeoutError, AttributeError):
266
266
  break
267
267
 
268
268
  async def htx_keepalive(self, interval=60):
@@ -274,7 +274,8 @@ class EventsDataStream:
274
274
  else:
275
275
  self.ping = 1
276
276
  self.logger.warning("From HTX server PING timeout exceeded")
277
- await self.websocket.close()
277
+ if self.websocket:
278
+ await self.websocket.close()
278
279
 
279
280
 
280
281
  class MarketEventsDataStream(EventsDataStream):
@@ -20,10 +20,10 @@ dynamic = ["version", "description"]
20
20
  requires-python = ">=3.10"
21
21
 
22
22
  dependencies = [
23
- "crypto-ws-api==2.1.3",
23
+ "crypto-ws-api==2.1.5",
24
24
  "pyotp==2.9.0",
25
25
  "simplejson==3.20.2",
26
- "aiohttp~=3.13.0",
26
+ "aiohttp~=3.13.2",
27
27
  "expiringdict~=1.2.2",
28
28
  "betterproto==2.0.0b7",
29
29
  "grpclib~=0.4.8"