├── bin ├── huobi_client └── huobi ├── huobi_client ├── __init__.py ├── socketIO_client │ ├── exceptions.py │ ├── symmetries.py │ ├── tests.py │ ├── transports.py │ └── __init__.py ├── streaming_client.py └── client.py ├── setup.py ├── .gitignore └── README.md /bin/huobi_client: -------------------------------------------------------------------------------- 1 | ../huobi_client -------------------------------------------------------------------------------- /huobi_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .streaming_client import StreamingClient 3 | -------------------------------------------------------------------------------- /huobi_client/socketIO_client/exceptions.py: -------------------------------------------------------------------------------- 1 | class SocketIOError(Exception): 2 | pass 3 | 4 | 5 | class ConnectionError(SocketIOError): 6 | pass 7 | 8 | 9 | class TimeoutError(SocketIOError): 10 | pass 11 | 12 | 13 | class PacketError(SocketIOError): 14 | pass 15 | -------------------------------------------------------------------------------- /huobi_client/socketIO_client/symmetries.py: -------------------------------------------------------------------------------- 1 | def _decode_safely(x): 2 | return x.decode('utf-8') if hasattr(x, 'decode') else x 3 | 4 | 5 | def _get_text(response): 6 | try: 7 | return response.text # requests 2.7.0 8 | except AttributeError: 9 | return response.content # requests 0.8.2 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="huobi_client", 5 | version="0.3.1", 6 | url='https://github.com/czheo/huobi-client-python', 7 | description="a client library for huobi", 8 | author="czheo", 9 | license="LGPL", 10 | keywords="bitcoin huobi huobi.com", 11 | packages=find_packages(), 12 | scripts=[ 13 | 'bin/huobi' 14 | ], 15 | install_requires=[ 16 | 'requests', 17 | 'six', 18 | 'websocket-client', 19 | ], 20 | ) 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | .venv/ 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # !! This library does not work, because Huobi's API has changed a lot since this library was developed. The author is not going to maintain it anymore. !! 2 | 3 | # huobi-client-python 4 | ## Installation 5 | ``` 6 | pip install huobi_client 7 | ``` 8 | 9 | ## Put access key and secret key in `~/.huobi.keys` 10 | 11 | ``` 12 | $ echo $access_key > ~/.huobi.keys 13 | $ echo $secet_key >> ~/.huobi.keys 14 | $ chmod 400 ~/.huobi.keys 15 | ``` 16 | 17 | `~/.huobi.keys` should look like below. 18 | ``` 19 | xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxx 20 | yyyyyyyy-yyyyyyyy-yyyyyyyy-yyyyy 21 | ``` 22 | 23 | ## Command Line Tool 24 | ``` 25 | $ huobi 26 | usage: huobi [-h] 27 | {info,orders,oinfo,buy,sell,buym,sellm,cancel,norders,tid2oid,avail_loans,loans,stream,kline,ticker,depth,market} 28 | ... 29 | 30 | huobi command line tool 31 | 32 | positional arguments: 33 | {info,orders,oinfo,buy,sell,buym,sellm,cancel,norders,tid2oid,avail_loans,loans,stream,kline,ticker,depth,market} 34 | info account info 35 | orders orders 36 | oinfo order info 37 | buy buy 38 | sell sell 39 | buym buy market 40 | sellm sell market 41 | cancel cancel order 42 | norders get new deal order 43 | tid2oid get order id by trade id 44 | avail_loans get available loans 45 | loans get loans 46 | stream dump socketio data 47 | kline get kline 48 | ticker get ticker 49 | depth get depth 50 | market get market detail 51 | 52 | optional arguments: 53 | -h, --help show this help message and exit 54 | ``` 55 | 56 | ## Library Usage 57 | 58 | ### Rest API 59 | ``` python 60 | from huobi_client import Client 61 | 62 | client = Client() 63 | 64 | client.get_account_info() # get account info 65 | client.get_orders() # get orders 66 | client.get_order_info(id) # get order info by order id 67 | client.buy(price, amount) # buy 68 | client.sell(price, amount) # sell 69 | client.buy_market(amount) # buy at market price 70 | client.sell_market(amount) # sell at market price 71 | client.cancel_order(id) # cancel order 72 | client.get_new_deal_orders() # get new deal orders 73 | client.withdraw_coin(address, amount) 74 | client.cancel_withdraw_coin(withdraw_coin_id) 75 | client.transfer(account_from, account_to, amount) 76 | client.loan(amount, loan_type) # loan_type = {cny, btc, ltc, usd} 77 | client.repay(loan_id, amount) 78 | client.get_loan_available() 79 | client.get_loans() 80 | ``` 81 | 82 | ### SocketIO API 83 | subscribe all messages 84 | ``` python 85 | from huobi_client import StreamingClient 86 | 87 | 88 | def on_message(data): 89 | print(data) 90 | 91 | sclient = StreamingClient() 92 | sclient.subscribe_all() 93 | sclient.connect(on_message) 94 | ``` 95 | subscribe specific messages 96 | ``` python 97 | from huobi_client import StreamingClient 98 | 99 | 100 | def on_message(data): 101 | print(data) 102 | 103 | sclient = StreamingClient() 104 | sclient.subscribe('tradeDetail') 105 | sclient.connect(on_message) 106 | ``` 107 | -------------------------------------------------------------------------------- /huobi_client/streaming_client.py: -------------------------------------------------------------------------------- 1 | from .socketIO_client import SocketIO 2 | import logging 3 | 4 | VALID_PERIOD = { 5 | "1min", "5min", "15min", "30min", "60min", 6 | "1day", "1week", "1mon", "1year", 7 | } 8 | VALID_PERCENT = {10, 20, 50, 80, 100} 9 | VALID_SYMBOL = { 10 | 'lastTimeLine', 11 | 'lastKLine', 12 | 'marketDepthDiff', 13 | 'marketDepthTopDiff', 14 | 'marketDetail', 15 | 'tradeDetail', 16 | 'marketOverview', 17 | } 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class StreamingClient: 22 | ''' 23 | sc = StreamingClient() 24 | sc.subscribe('lastTimeLine') 25 | sc.run(on_msg) 26 | ''' 27 | def __init__(self): 28 | self._io = SocketIO('hq.huobi.com', 80) 29 | self.req_data = { 30 | 'version': 1, 31 | 'msgType': 'reqMsgSubscribe', 32 | 'symbolList': {} 33 | } 34 | 35 | def subscribe(self, sym, period='1min', percent=10, currency='btccny'): 36 | symbol_list = self.req_data['symbolList'] 37 | if sym in VALID_SYMBOL: 38 | d = { 39 | 'symbolId': currency, 40 | 'pushType': 'pushLong', 41 | } 42 | if sym == 'lastKLine': 43 | if period in VALID_PERIOD: 44 | d['period'] = period 45 | else: 46 | raise Exception('period must be in %s' 47 | % VALID_PERIOD) 48 | elif sym == 'marketDepthDiff': 49 | if percent in VALID_PERCENT: 50 | d['percent'] = '%d' % percent 51 | else: 52 | raise Exception('percent must be in %s' 53 | % VALID_PERCENT) 54 | # init symbol_list[sym] 55 | if sym not in symbol_list: 56 | symbol_list[sym] = [] 57 | # append record 58 | if d not in symbol_list[sym]: 59 | symbol_list[sym].append(d) 60 | else: 61 | raise Exception('"%s" is not a valid symbol.' 62 | 'symbol must be in %s' 63 | % (sym, set(symbol_list.keys()))) 64 | 65 | def subscribe_all(self, currency='btccny'): 66 | for sym in VALID_SYMBOL: 67 | if sym == 'lastKLine': 68 | for period in VALID_PERIOD: 69 | self.subscribe(sym, period=period, 70 | currency=currency) 71 | elif sym == 'marketDepthTopDiff': 72 | for percent in VALID_PERCENT: 73 | self.subscribe(sym, percent=percent, 74 | currency=currency) 75 | else: 76 | self.subscribe(sym, currency=currency) 77 | 78 | def unsubscribe(self, sym, period='1min', 79 | percent=10, currency='btccny'): 80 | raise NotImplementedError 81 | 82 | def _on_connect(self): 83 | logger.info('connect') 84 | self._io.emit('request', self.req_data) 85 | 86 | def _on_reconnect(self): 87 | logger.info('reconnected') 88 | 89 | def _on_disconnect(self): 90 | logger.info('disconnected') 91 | 92 | def _on_request(self, data): 93 | logger.info('request: %s' % data) 94 | 95 | def connect(self, on_msg): 96 | self._io.on('connect', self._on_connect) 97 | self._io.on('reconnect', self._on_reconnect) 98 | self._io.on('disconnect', self._on_disconnect) 99 | self._io.on('request', self._on_request) 100 | self._io.on('message', on_msg) 101 | self._io.wait() 102 | 103 | # unused 104 | # def timeline(on_msg, currency='btccny'): 105 | # data = { 106 | # 'msgType': 'reqTimeLine', 107 | # } 108 | # _run_client(on_msg, data, currency) 109 | # 110 | # 111 | # def kline(on_msg, period='1min', currency='btccny'): 112 | # data = { 113 | # 'msgType': 'reqKLine', 114 | # 'period': period, 115 | # } 116 | # _run_client(on_msg, data, currency) 117 | # 118 | # 119 | # def market_depth_top(on_msg, currency='btccny'): 120 | # data = { 121 | # 'msgType': 'reqMarketDepthTop', 122 | # } 123 | # _run_client(on_msg, data, currency) 124 | # 125 | # 126 | # def market_depth(on_msg, percent=10, currency='btccny'): 127 | # data = { 128 | # 'msgType': 'reqMarketDepth', 129 | # 'percent': percent, 130 | # } 131 | # _run_client(on_msg, data, currency) 132 | # 133 | # 134 | # def trade_detail_top(on_msg, count=None, currency='btccny'): 135 | # data = { 136 | # 'msgType': 'reqTradeDetailTop', 137 | # } 138 | # if count: 139 | # data['count'] = count 140 | # _run_client(on_msg, data, currency) 141 | # 142 | # 143 | # def market_detail(on_msg, currency='btccny'): 144 | # data = { 145 | # 'msgType': 'reqMarketDetail', 146 | # } 147 | # _run_client(on_msg, data, currency) 148 | # 149 | # 150 | # def _run_client(on_msg, data, currency): 151 | # sc = StreamingClient() 152 | # data['symbolId'] = currency 153 | # sc.req_data.update(data) 154 | # del sc.req_data['symbolList'] 155 | # print(sc.req_data) 156 | # sc.run(on_msg) 157 | -------------------------------------------------------------------------------- /bin/huobi: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from huobi_client import Client 4 | from huobi_client import StreamingClient 5 | from huobi_client.streaming_client import VALID_SYMBOL,\ 6 | VALID_PERIOD, VALID_PERCENT 7 | from pprint import pprint 8 | import argparse 9 | import logging 10 | 11 | 12 | def on_msg(data): 13 | pprint(data) 14 | 15 | 16 | def stream(args): 17 | logging.basicConfig(level=logging.INFO) 18 | sc = StreamingClient() 19 | if args.filter: 20 | # subscribe filter 21 | if args.period: 22 | sc.subscribe(args.filter, period=args.period) 23 | elif args.percent: 24 | sc.subscribe(args.filter, percent=args.percent) 25 | else: 26 | sc.subscribe(args.filter) 27 | else: 28 | # subscribe all 29 | sc.subscribe_all() 30 | 31 | # connect to server 32 | try: 33 | sc.connect(on_msg) 34 | except KeyboardInterrupt: 35 | exit() 36 | 37 | 38 | def main(args): 39 | c = Client() 40 | if args.command == 'stream': 41 | stream(args) 42 | elif args.command == 'info': 43 | pprint(c.get_account_info()) 44 | elif args.command == 'orders': 45 | pprint(c.get_orders()) 46 | elif args.command == 'oinfo': 47 | pprint(c.get_order_info(args.id)) 48 | elif args.command == 'buy': 49 | pprint(c.buy(args.price, args.amount)) 50 | elif args.command == 'sell': 51 | pprint(c.sell(args.price, args.amount)) 52 | elif args.command == 'buym': 53 | pprint(c.buy_market(args.amount)) 54 | elif args.command == 'sellm': 55 | pprint(c.sell_market(args.amount)) 56 | elif args.command == 'cancel': 57 | pprint(c.cancel_order(args.id)) 58 | elif args.command == 'norders': 59 | pprint(c.get_new_deal_orders()) 60 | elif args.command == 'tid2oid': 61 | pprint(c.get_order_id_by_trade_id(args.tid)) 62 | elif args.command == 'avail_loans': 63 | pprint(c.get_loan_available()) 64 | elif args.command == 'loans': 65 | pprint(c.get_loans()) 66 | elif args.command == 'kline': 67 | if args.period: 68 | pprint(c.get_kline(args.period)) 69 | else: 70 | pprint(c.get_kline()) 71 | elif args.command == 'ticker': 72 | pprint(c.get_ticker()) 73 | elif args.command == 'depth': 74 | if args.count: 75 | pprint(c.get_depth(args.count)) 76 | else: 77 | pprint(c.get_depth()) 78 | elif args.command == 'market': 79 | pprint(c.get_market()) 80 | 81 | 82 | if __name__ == "__main__": 83 | parser = argparse.ArgumentParser(prog='huobi', 84 | description='huobi command line tool') 85 | subparsers = parser.add_subparsers(dest='command') 86 | # info 87 | parser_info = subparsers.add_parser('info', help='account info') 88 | # orders 89 | parser_orders = subparsers.add_parser('orders', help='orders') 90 | # oinfo 91 | parser_oinfo = subparsers.add_parser('oinfo', help='order info') 92 | parser_oinfo.add_argument('id') 93 | # buy 94 | parser_buy = subparsers.add_parser('buy', help='buy') 95 | parser_buy.add_argument('price', type=float) 96 | parser_buy.add_argument('amount', type=float) 97 | # sell 98 | parser_sell = subparsers.add_parser('sell', help='sell') 99 | parser_sell.add_argument('price', type=float) 100 | parser_sell.add_argument('amount', type=float) 101 | # buy_market 102 | parser_buym = subparsers.add_parser('buym', help='buy market') 103 | parser_buym.add_argument('amount', type=float) 104 | # sell_market 105 | parser_sellm = subparsers.add_parser('sellm', help='sell market') 106 | parser_sellm.add_argument('amount', type=float) 107 | # cancel order 108 | parser_cancel = subparsers.add_parser('cancel', help='cancel order') 109 | parser_cancel.add_argument('id') 110 | # get new deal orders 111 | parser_new_orders = subparsers.add_parser('norders', 112 | help='get new deal order') 113 | # get order id by trade id 114 | parser_tid2oid = subparsers.add_parser('tid2oid', 115 | help='get order id by trade id') 116 | parser_tid2oid.add_argument('tid') 117 | 118 | # TODO: withdraw 119 | # parser_withdraw = subparsers.add_parser('withdraw', 120 | # help='withdraw related functions') 121 | # TODO: loans 122 | # TODO: transfer 123 | 124 | # get available loans 125 | parser_avail_loan = subparsers.add_parser('avail_loans', 126 | help='get available loans') 127 | 128 | # get loans 129 | parser_loans = subparsers.add_parser('loans', 130 | help='get loans') 131 | 132 | # stream 133 | parser_stream = subparsers.add_parser('stream', 134 | help='dump socketio data') 135 | parser_stream.add_argument('-f', '--filter', choices=VALID_SYMBOL) 136 | parser_stream.add_argument('--period', choices=VALID_PERIOD) 137 | parser_stream.add_argument('--percent', choices=VALID_PERCENT) 138 | 139 | # kline 140 | parser_kline = subparsers.add_parser('kline', help='get kline') 141 | parser_kline.add_argument('--period', choices=VALID_PERIOD) 142 | 143 | # ticker 144 | parser_ticker = subparsers.add_parser('ticker', help='get ticker') 145 | 146 | # depth 147 | parser_depth = subparsers.add_parser('depth', help='get depth') 148 | parser_depth.add_argument('-c', '--count', type=int) 149 | 150 | # market 151 | parser_market = subparsers.add_parser('market', help='get market detail') 152 | 153 | args = parser.parse_args() 154 | if args.command: 155 | main(args) 156 | else: 157 | parser.print_help() 158 | -------------------------------------------------------------------------------- /huobi_client/socketIO_client/tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from unittest import TestCase 4 | 5 | from . import SocketIO, LoggingNamespace, find_callback 6 | from .transports import TIMEOUT_IN_SECONDS 7 | 8 | 9 | HOST = 'localhost' 10 | PORT = 8000 11 | DATA = 'xxx' 12 | PAYLOAD = {'xxx': 'yyy'} 13 | logging.basicConfig(level=logging.DEBUG) 14 | 15 | 16 | class BaseMixin(object): 17 | 18 | def setUp(self): 19 | self.called_on_response = False 20 | 21 | def tearDown(self): 22 | del self.socketIO 23 | 24 | def on_response(self, *args): 25 | for arg in args: 26 | if isinstance(arg, dict): 27 | self.assertEqual(arg, PAYLOAD) 28 | else: 29 | self.assertEqual(arg, DATA) 30 | self.called_on_response = True 31 | 32 | def test_disconnect(self): 33 | 'Disconnect' 34 | self.socketIO.define(LoggingNamespace) 35 | self.assertTrue(self.socketIO.connected) 36 | self.socketIO.disconnect() 37 | self.assertFalse(self.socketIO.connected) 38 | # Use context manager 39 | with SocketIO(HOST, PORT, Namespace) as self.socketIO: 40 | namespace = self.socketIO.get_namespace() 41 | self.assertFalse(namespace.called_on_disconnect) 42 | self.assertTrue(self.socketIO.connected) 43 | self.assertTrue(namespace.called_on_disconnect) 44 | self.assertFalse(self.socketIO.connected) 45 | 46 | def test_message(self): 47 | 'Message' 48 | namespace = self.socketIO.define(Namespace) 49 | self.socketIO.message() 50 | self.socketIO.wait(self.wait_time_in_seconds) 51 | self.assertEqual(namespace.response, 'message_response') 52 | 53 | def test_message_with_data(self): 54 | 'Message with data' 55 | namespace = self.socketIO.define(Namespace) 56 | self.socketIO.message(DATA) 57 | self.socketIO.wait(self.wait_time_in_seconds) 58 | self.assertEqual(namespace.response, DATA) 59 | 60 | def test_message_with_payload(self): 61 | 'Message with payload' 62 | namespace = self.socketIO.define(Namespace) 63 | self.socketIO.message(PAYLOAD) 64 | self.socketIO.wait(self.wait_time_in_seconds) 65 | self.assertEqual(namespace.response, PAYLOAD) 66 | 67 | def test_message_with_callback(self): 68 | 'Message with callback' 69 | self.socketIO.define(LoggingNamespace) 70 | self.socketIO.message(callback=self.on_response) 71 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 72 | self.assertTrue(self.called_on_response) 73 | 74 | def test_message_with_callback_with_data(self): 75 | 'Message with callback with data' 76 | self.socketIO.define(LoggingNamespace) 77 | self.socketIO.message(DATA, self.on_response) 78 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 79 | self.assertTrue(self.called_on_response) 80 | 81 | def test_emit(self): 82 | 'Emit' 83 | namespace = self.socketIO.define(Namespace) 84 | self.socketIO.emit('emit') 85 | self.socketIO.wait(self.wait_time_in_seconds) 86 | self.assertEqual(namespace.args_by_event, { 87 | 'emit_response': (), 88 | }) 89 | 90 | def test_emit_with_payload(self): 91 | 'Emit with payload' 92 | namespace = self.socketIO.define(Namespace) 93 | self.socketIO.emit('emit_with_payload', PAYLOAD) 94 | self.socketIO.wait(self.wait_time_in_seconds) 95 | self.assertEqual(namespace.args_by_event, { 96 | 'emit_with_payload_response': (PAYLOAD,), 97 | }) 98 | 99 | def test_emit_with_multiple_payloads(self): 100 | 'Emit with multiple payloads' 101 | namespace = self.socketIO.define(Namespace) 102 | self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) 103 | self.socketIO.wait(self.wait_time_in_seconds) 104 | self.assertEqual(namespace.args_by_event, { 105 | 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), 106 | }) 107 | 108 | def test_emit_with_callback(self): 109 | 'Emit with callback' 110 | self.socketIO.define(LoggingNamespace) 111 | self.socketIO.emit('emit_with_callback', self.on_response) 112 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 113 | self.assertTrue(self.called_on_response) 114 | 115 | def test_emit_with_callback_with_payload(self): 116 | 'Emit with callback with payload' 117 | self.socketIO.define(LoggingNamespace) 118 | self.socketIO.emit( 119 | 'emit_with_callback_with_payload', self.on_response) 120 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 121 | self.assertTrue(self.called_on_response) 122 | 123 | def test_emit_with_callback_with_multiple_payloads(self): 124 | 'Emit with callback with multiple payloads' 125 | self.socketIO.define(LoggingNamespace) 126 | self.socketIO.emit( 127 | 'emit_with_callback_with_multiple_payloads', self.on_response) 128 | self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds) 129 | self.assertTrue(self.called_on_response) 130 | 131 | def test_emit_with_event(self): 132 | 'Emit to trigger an event' 133 | self.socketIO.on('emit_with_event_response', self.on_response) 134 | self.socketIO.emit('emit_with_event', PAYLOAD) 135 | self.socketIO.wait(self.wait_time_in_seconds) 136 | self.assertTrue(self.called_on_response) 137 | 138 | def test_ack(self): 139 | 'Trigger server callback' 140 | namespace = self.socketIO.define(Namespace) 141 | self.socketIO.emit('ack', PAYLOAD) 142 | self.socketIO.wait(self.wait_time_in_seconds) 143 | self.assertEqual(namespace.args_by_event, { 144 | 'ack_response': (PAYLOAD,), 145 | 'ack_callback_response': (PAYLOAD,), 146 | }) 147 | 148 | def test_wait_with_disconnect(self): 149 | 'Exit loop when the client wants to disconnect' 150 | self.socketIO.define(Namespace) 151 | self.socketIO.emit('wait_with_disconnect') 152 | timeout_in_seconds = 5 153 | start_time = time.time() 154 | self.socketIO.wait(timeout_in_seconds) 155 | self.assertTrue(time.time() - start_time < timeout_in_seconds) 156 | 157 | def test_namespace_emit(self): 158 | 'Behave differently in different namespaces' 159 | main_namespace = self.socketIO.define(Namespace) 160 | chat_namespace = self.socketIO.define(Namespace, '/chat') 161 | news_namespace = self.socketIO.define(Namespace, '/news') 162 | news_namespace.emit('emit_with_payload', PAYLOAD) 163 | self.socketIO.wait(self.wait_time_in_seconds) 164 | self.assertEqual(main_namespace.args_by_event, {}) 165 | self.assertEqual(chat_namespace.args_by_event, {}) 166 | self.assertEqual(news_namespace.args_by_event, { 167 | 'emit_with_payload_response': (PAYLOAD,), 168 | }) 169 | 170 | def test_namespace_ack(self): 171 | 'Trigger server callback' 172 | chat_namespace = self.socketIO.define(Namespace, '/chat') 173 | chat_namespace.emit('ack', PAYLOAD) 174 | self.socketIO.wait(self.wait_time_in_seconds) 175 | self.assertEqual(chat_namespace.args_by_event, { 176 | 'ack_response': (PAYLOAD,), 177 | 'ack_callback_response': (PAYLOAD,), 178 | }) 179 | 180 | 181 | class Test_WebsocketTransport(TestCase, BaseMixin): 182 | 183 | def setUp(self): 184 | super(Test_WebsocketTransport, self).setUp() 185 | self.socketIO = SocketIO(HOST, PORT, transports=['websocket']) 186 | self.wait_time_in_seconds = 0.1 187 | 188 | 189 | class Test_XHR_PollingTransport(TestCase, BaseMixin): 190 | 191 | def setUp(self): 192 | super(Test_XHR_PollingTransport, self).setUp() 193 | self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling']) 194 | self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 195 | 196 | 197 | class Test_JSONP_PollingTransport(TestCase, BaseMixin): 198 | 199 | def setUp(self): 200 | super(Test_JSONP_PollingTransport, self).setUp() 201 | self.socketIO = SocketIO(HOST, PORT, transports=['jsonp-polling']) 202 | self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1 203 | 204 | 205 | class Namespace(LoggingNamespace): 206 | 207 | def initialize(self): 208 | self.response = None 209 | self.args_by_event = {} 210 | self.called_on_disconnect = False 211 | 212 | def on_disconnect(self): 213 | self.called_on_disconnect = True 214 | 215 | def on_message(self, data): 216 | self.response = data 217 | 218 | def on_event(self, event, *args): 219 | callback, args = find_callback(args) 220 | if callback: 221 | callback(*args) 222 | self.args_by_event[event] = args 223 | 224 | def on_wait_with_disconnect_response(self): 225 | self.disconnect() 226 | -------------------------------------------------------------------------------- /huobi_client/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import hashlib 4 | import requests 5 | try: 6 | # py3 7 | from urllib.parse import urlencode 8 | except: 9 | # py2 10 | from urllib import urlencode 11 | 12 | HUOBI_API = "https://api.huobi.com/" 13 | 14 | def _period_str_to_url_param(period): 15 | d = { 16 | '1min': '001', 17 | '5min': '005', 18 | '15min': '015', 19 | '30min': '030', 20 | '60min': '060', 21 | '1day': '100', 22 | '1week': '200', 23 | '1mon': '300', 24 | '1year': '400', 25 | } 26 | 27 | return d.get(period) 28 | 29 | def _signature(params): 30 | params = sorted(params.items()) 31 | message = urlencode(params).encode('utf8') 32 | m = hashlib.md5() 33 | m.update(message) 34 | return m.hexdigest() 35 | 36 | 37 | def _account_type_to_id(account_type): 38 | d = { 39 | 'cny': 1, 40 | 'usd': 2, 41 | } 42 | return d.get(account_type) 43 | 44 | 45 | def _set_coin_type(params, coin_type): 46 | d = { 47 | 'btc': 1, 48 | 'ltc': 2, 49 | } 50 | params['coin_type'] = d.get(coin_type, 1) 51 | 52 | 53 | def _loan_type_to_id(loan_type): 54 | d = { 55 | 'cny': 1, 56 | 'btc': 2, 57 | 'ltc': 3, 58 | 'usd': 4, 59 | } 60 | return d.get(loan_type) 61 | 62 | 63 | class Client: 64 | def __init__(self): 65 | key_path = os.path.join(os.path.expanduser('~'), '.huobi.keys') 66 | with open(key_path) as f: 67 | self.access_key, self.secret_key = f.read().splitlines() 68 | 69 | def _request(self, params): 70 | # delete and save params that not participate in signature 71 | skip_params = { 72 | 'market': None, 73 | 'trade_password': None, 74 | 'trade_id': None, 75 | 'repay_all': None, 76 | } 77 | for k, v in list(params.items()): 78 | if k in skip_params: 79 | skip_params[k] = v 80 | del params[k] 81 | 82 | params['access_key'] = self.access_key 83 | params['secret_key'] = self.secret_key 84 | params['created'] = int(time.time()) 85 | params['sign'] = _signature(params) 86 | # NOTE: secret_key should not be in signature 87 | del params['secret_key'] 88 | 89 | # put skipped params back 90 | params.update(skip_params) 91 | # delete None params 92 | params = {k: v for k, v in params.items() if v is not None} 93 | return requests.get(HUOBI_API + 'apiv3', params).json() 94 | 95 | def get_account_info(self): 96 | params = { 97 | 'method': 'get_account_info', 98 | } 99 | return self._request(params) 100 | 101 | def get_orders(self, coin_type='btc'): 102 | params = { 103 | 'method': 'get_orders', 104 | } 105 | _set_coin_type(params, coin_type) 106 | return self._request(params) 107 | 108 | def get_order_info(self, id, coin_type='btc'): 109 | params = { 110 | 'method': 'order_info', 111 | 'id': id 112 | } 113 | _set_coin_type(params, coin_type) 114 | return self._request(params) 115 | 116 | def buy(self, price, amount, trade_password=None, 117 | trade_id=None, coin_type='btc'): 118 | params = { 119 | 'method': 'buy', 120 | 'price': price, 121 | 'amount': amount, 122 | 'trade_password': trade_password, 123 | 'trade_id': trade_id, 124 | } 125 | _set_coin_type(params, coin_type) 126 | return self._request(params) 127 | 128 | def sell(self, price, amount, trade_password=None, 129 | trade_id=None, coin_type='btc'): 130 | params = { 131 | 'method': 'sell', 132 | 'price': price, 133 | 'amount': amount, 134 | 'trade_password': trade_password, 135 | 'trade_id': trade_id, 136 | } 137 | _set_coin_type(params, coin_type) 138 | return self._request(params) 139 | 140 | def buy_market(self, amount, trade_password=None, 141 | trade_id=None, coin_type='btc'): 142 | ''' 143 | amount: amount of cny 144 | ''' 145 | params = { 146 | 'method': 'buy_market', 147 | 'amount': amount, 148 | 'trade_password': trade_password, 149 | 'trade_id': trade_id, 150 | } 151 | _set_coin_type(params, coin_type) 152 | return self._request(params) 153 | 154 | def sell_market(self, amount, trade_password=None, 155 | trade_id=None, coin_type='btc'): 156 | ''' 157 | amount: amount of btc 158 | ''' 159 | params = { 160 | 'method': 'sell_market', 161 | 'amount': amount, 162 | 'trade_password': trade_password, 163 | 'trade_id': trade_id, 164 | } 165 | _set_coin_type(params, coin_type) 166 | return self._request(params) 167 | 168 | def cancel_order(self, id, coin_type='btc'): 169 | params = { 170 | 'method': 'cancel_order', 171 | 'id': id, 172 | } 173 | _set_coin_type(params, coin_type) 174 | return self._request(params) 175 | 176 | def get_new_deal_orders(self, coin_type='btc'): 177 | params = { 178 | 'method': 'get_new_deal_orders' 179 | } 180 | _set_coin_type(params, coin_type) 181 | return self._request(params) 182 | 183 | def get_order_id_by_trade_id(self, trade_id, coin_type='btc'): 184 | params = { 185 | 'method': 'get_new_deal_orders', 186 | 'trade_id': trade_id, 187 | } 188 | _set_coin_type(params, coin_type) 189 | return self._request(params) 190 | 191 | def withdraw_coin(self, address, amount, trade_pwd=None, coin_type='btc'): 192 | params = { 193 | 'method': 'withdraw_coin', 194 | 'withdraw_address': address, 195 | 'withdraw_amount': amount, 196 | 'trade_password': trade_pwd, 197 | # withdraw_fee: useless 198 | } 199 | _set_coin_type(params, coin_type) 200 | return self._request(params) 201 | 202 | def cancel_withdraw_coin(self, withdraw_coin_id): 203 | params = { 204 | 'method': 'cancel_withdraw_coin', 205 | 'withdraw_coin_id': withdraw_coin_id, 206 | } 207 | return self._request(params) 208 | 209 | def get_withdraw_coin_result(self, withdraw_coin_id): 210 | params = { 211 | 'method': 'get_withdraw_coin_result', 212 | 'withdraw_coin_id': withdraw_coin_id, 213 | } 214 | return self._request(params) 215 | 216 | def transfer(self, account_from, account_to, amount): 217 | params = { 218 | 'method': 'transfer', 219 | 'account_from': _account_type_to_id(account_from), 220 | 'account_to': _account_type_to_id(account_to), 221 | 'amount': amount 222 | } 223 | return self._request(params) 224 | 225 | def loan(self, amount, loan_type): 226 | params = { 227 | 'method': 'loan', 228 | 'amount': amount, 229 | 'loan_type': _loan_type_to_id(loan_type), 230 | } 231 | return self._request(params) 232 | 233 | def repay(self, loan_id, amount, repay_all=False): 234 | params = { 235 | 'method': 'repayment', 236 | 'loan_id': loan_id, 237 | 'amount': amount, 238 | 'repay_all': 1 if repay_all else 0, 239 | } 240 | return self._request(params) 241 | 242 | def get_loan_available(self): 243 | params = { 244 | 'method': 'get_loan_available', 245 | } 246 | return self._request(params) 247 | 248 | def get_loans(self): 249 | params = { 250 | 'method': 'get_loans', 251 | } 252 | return self._request(params) 253 | 254 | 255 | # ================ 256 | # market api 257 | # ================ 258 | def get_kline(self, period='1min', currency='cny', coin_type='btc'): 259 | ''' 260 | timestamp, open, high, low, close, volume 261 | ''' 262 | if currency == 'usd': 263 | market = 'usd' 264 | else: 265 | market = 'static' 266 | period_str = _period_str_to_url_param(period) 267 | url = HUOBI_API + '{}market/{}_kline_{}_json.js'.format(market, coin_type, period_str) 268 | return requests.get(url).json() 269 | 270 | def get_ticker(self, currency='cny', coin_type='btc'): 271 | if currency == 'usd': 272 | market = 'usd' 273 | else: 274 | market = 'static' 275 | url = HUOBI_API + '{}market/ticker_{}_json.js'.format(market, coin_type) 276 | return requests.get(url).json() 277 | 278 | def get_depth(self, count=9999, currency='cny', coin_type='btc'): 279 | if currency == 'usd': 280 | market = 'usd' 281 | else: 282 | market = 'static' 283 | 284 | if not count in range(1, 151): 285 | count = 'json' 286 | 287 | url = HUOBI_API + '{}market/depth_{}_{}.js'.format(market, coin_type, count) 288 | return requests.get(url).json() 289 | 290 | def get_market(self, currency='cny', coin_type='btc'): 291 | if currency == 'usd': 292 | market = 'usd' 293 | else: 294 | market = 'static' 295 | 296 | url = HUOBI_API + '{}market/detail_{}_json.js'.format(market, coin_type) 297 | return requests.get(url).json() 298 | -------------------------------------------------------------------------------- /huobi_client/socketIO_client/transports.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import json 3 | import logging 4 | import re 5 | import requests 6 | import six 7 | import socket 8 | import sys 9 | import time 10 | import websocket 11 | 12 | from .exceptions import ConnectionError, TimeoutError 13 | from .symmetries import _decode_safely, _get_text 14 | 15 | 16 | if not hasattr(websocket, 'create_connection'): 17 | sys.exit("""Incompatible websocket implementation 18 | - Please make sure that you have websocket-client installed 19 | - Please remove other websocket implementations""") 20 | 21 | 22 | TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling' 23 | BOUNDARY = six.u('\ufffd') 24 | TIMEOUT_IN_SECONDS = 3 25 | _log = logging.getLogger(__name__) 26 | escape_unicode = lambda x: codecs.getdecoder('unicode_escape')(x)[0] 27 | try: 28 | unicode 29 | except NameError: 30 | encode_unicode = lambda x: x 31 | else: 32 | encode_unicode = lambda x: unicode(x).encode('utf-8') 33 | 34 | 35 | class _AbstractTransport(object): 36 | 37 | def __init__(self): 38 | self._packet_id = 0 39 | self._callback_by_packet_id = {} 40 | self._wants_to_disconnect = False 41 | self._packets = [] 42 | 43 | def _log(self, level, msg, *attrs): 44 | _log.log(level, '[%s] %s' % (self._url, msg), *[ 45 | _decode_safely(x) for x in attrs]) 46 | 47 | def disconnect(self, path=''): 48 | if not path: 49 | self._wants_to_disconnect = True 50 | if not self.connected: 51 | return 52 | if path: 53 | self.send_packet(0, path) 54 | else: 55 | self.close() 56 | 57 | def connect(self, path): 58 | self.send_packet(1, path) 59 | 60 | def send_heartbeat(self): 61 | self.send_packet(2) 62 | 63 | def message(self, path, data, callback): 64 | if isinstance(data, six.string_types): 65 | code = 3 66 | else: 67 | code = 4 68 | data = json.dumps(data, ensure_ascii=False) 69 | self.send_packet(code, path, data, callback) 70 | 71 | def emit(self, path, event, args, callback): 72 | data = json.dumps(dict(name=event, args=args), ensure_ascii=False) 73 | self.send_packet(5, path, data, callback) 74 | 75 | def ack(self, path, packet_id, *args): 76 | packet_id = packet_id.rstrip('+') 77 | data = '%s+%s' % ( 78 | packet_id, 79 | json.dumps(args, ensure_ascii=False), 80 | ) if args else packet_id 81 | self.send_packet(6, path, data) 82 | 83 | def noop(self, path=''): 84 | self.send_packet(8, path) 85 | 86 | def send_packet(self, code, path='', data='', callback=None): 87 | packet_id = self.set_ack_callback(callback) if callback else '' 88 | packet_parts = str(code), packet_id, path, encode_unicode(data) 89 | packet_text = ':'.join(packet_parts) 90 | self.send(packet_text) 91 | self._log(logging.DEBUG, '[packet sent] %s', packet_text) 92 | 93 | def recv_packet(self, timeout=None): 94 | try: 95 | while self._packets: 96 | yield self._packets.pop(0) 97 | except IndexError: 98 | pass 99 | for packet_text in self.recv(timeout=timeout): 100 | self._log(logging.DEBUG, '[packet received] %s', packet_text) 101 | try: 102 | packet_parts = packet_text.split(':', 3) 103 | except AttributeError: 104 | self._log(logging.WARNING, '[packet error] %s', packet_text) 105 | continue 106 | code, packet_id, path, data = None, None, None, None 107 | packet_count = len(packet_parts) 108 | if 4 == packet_count: 109 | code, packet_id, path, data = packet_parts 110 | elif 3 == packet_count: 111 | code, packet_id, path = packet_parts 112 | elif 1 == packet_count: 113 | code = packet_parts[0] 114 | yield code, packet_id, path, data 115 | 116 | def _enqueue_packet(self, packet): 117 | self._packets.append(packet) 118 | 119 | def set_ack_callback(self, callback): 120 | 'Set callback to be called after server sends an acknowledgment' 121 | self._packet_id += 1 122 | self._callback_by_packet_id[str(self._packet_id)] = callback 123 | return '%s+' % self._packet_id 124 | 125 | def get_ack_callback(self, packet_id): 126 | 'Get callback to be called after server sends an acknowledgment' 127 | callback = self._callback_by_packet_id[packet_id] 128 | del self._callback_by_packet_id[packet_id] 129 | return callback 130 | 131 | @property 132 | def has_ack_callback(self): 133 | return True if self._callback_by_packet_id else False 134 | 135 | 136 | class _WebsocketTransport(_AbstractTransport): 137 | 138 | def __init__(self, socketIO_session, is_secure, base_url, **kw): 139 | super(_WebsocketTransport, self).__init__() 140 | url = '%s://%s/websocket/%s' % ( 141 | 'wss' if is_secure else 'ws', 142 | base_url, socketIO_session.id) 143 | self._url = url 144 | http_session = _prepare_http_session(kw) 145 | req = http_session.prepare_request(requests.Request('GET', url)) 146 | headers = ['%s: %s' % item for item in req.headers.items()] 147 | try: 148 | self._connection = websocket.create_connection(url, header=headers) 149 | except socket.timeout as e: 150 | raise ConnectionError(e) 151 | except socket.error as e: 152 | raise ConnectionError(e) 153 | self._connection.settimeout(TIMEOUT_IN_SECONDS) 154 | 155 | @property 156 | def connected(self): 157 | return self._connection.connected 158 | 159 | def send(self, packet_text): 160 | try: 161 | self._connection.send(packet_text) 162 | except websocket.WebSocketTimeoutException as e: 163 | message = 'timed out while sending %s (%s)' % (packet_text, e) 164 | self._log(logging.WARNING, message) 165 | raise TimeoutError(e) 166 | except socket.error as e: 167 | message = 'disconnected while sending %s (%s)' % (packet_text, e) 168 | self._log(logging.WARNING, message) 169 | raise ConnectionError(message) 170 | 171 | def recv(self, timeout=None): 172 | if timeout: 173 | self._connection.settimeout(timeout) 174 | try: 175 | yield self._connection.recv() 176 | except websocket.WebSocketTimeoutException as e: 177 | raise TimeoutError(e) 178 | except websocket.SSLError as e: 179 | if 'timed out' in e.message: 180 | raise TimeoutError(e) 181 | else: 182 | raise ConnectionError(e) 183 | except websocket.WebSocketConnectionClosedException as e: 184 | raise ConnectionError('connection closed (%s)' % e) 185 | except socket.error as e: 186 | raise ConnectionError(e) 187 | 188 | def close(self): 189 | self._connection.close() 190 | 191 | 192 | class _XHR_PollingTransport(_AbstractTransport): 193 | 194 | def __init__(self, socketIO_session, is_secure, base_url, **kw): 195 | super(_XHR_PollingTransport, self).__init__() 196 | self._url = '%s://%s/xhr-polling/%s' % ( 197 | 'https' if is_secure else 'http', 198 | base_url, socketIO_session.id) 199 | self._connected = True 200 | self._http_session = _prepare_http_session(kw) 201 | # Create connection 202 | for packet in self.recv_packet(): 203 | self._enqueue_packet(packet) 204 | 205 | @property 206 | def connected(self): 207 | return self._connected 208 | 209 | @property 210 | def _params(self): 211 | return dict(t=int(time.time())) 212 | 213 | def send(self, packet_text): 214 | _get_response( 215 | self._http_session.post, 216 | self._url, 217 | params=self._params, 218 | data=packet_text, 219 | timeout=TIMEOUT_IN_SECONDS) 220 | 221 | def recv(self, timeout=None): 222 | response = _get_response( 223 | self._http_session.get, 224 | self._url, 225 | params=self._params, 226 | timeout=timeout or TIMEOUT_IN_SECONDS, 227 | stream=True) 228 | response_text = _get_text(response) 229 | if not response_text.startswith(BOUNDARY): 230 | yield response_text 231 | return 232 | for packet_text in _yield_text_from_framed_data(response_text): 233 | yield packet_text 234 | 235 | def close(self): 236 | _get_response( 237 | self._http_session.get, 238 | self._url, 239 | params=dict(list(self._params.items()) + [('disconnect', True)])) 240 | self._connected = False 241 | 242 | 243 | class _JSONP_PollingTransport(_AbstractTransport): 244 | 245 | RESPONSE_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);') 246 | 247 | def __init__(self, socketIO_session, is_secure, base_url, **kw): 248 | super(_JSONP_PollingTransport, self).__init__() 249 | self._url = '%s://%s/jsonp-polling/%s' % ( 250 | 'https' if is_secure else 'http', 251 | base_url, socketIO_session.id) 252 | self._connected = True 253 | self._http_session = _prepare_http_session(kw) 254 | self._id = 0 255 | # Create connection 256 | for packet in self.recv_packet(): 257 | self._enqueue_packet(packet) 258 | 259 | @property 260 | def connected(self): 261 | return self._connected 262 | 263 | @property 264 | def _params(self): 265 | return dict(t=int(time.time()), i=self._id) 266 | 267 | def send(self, packet_text): 268 | _get_response( 269 | self._http_session.post, 270 | self._url, 271 | params=self._params, 272 | data='d=%s' % requests.utils.quote(json.dumps(packet_text)), 273 | headers={'content-type': 'application/x-www-form-urlencoded'}, 274 | timeout=TIMEOUT_IN_SECONDS) 275 | 276 | def recv(self, timeout=None): 277 | 'Decode the JavaScript response so that we can parse it as JSON' 278 | response = _get_response( 279 | self._http_session.get, 280 | self._url, 281 | params=self._params, 282 | headers={'content-type': 'text/javascript; charset=UTF-8'}, 283 | timeout=timeout or TIMEOUT_IN_SECONDS) 284 | response_text = _get_text(response) 285 | try: 286 | self._id, response_text = self.RESPONSE_PATTERN.match( 287 | response_text).groups() 288 | except AttributeError: 289 | self._log(logging.WARNING, '[packet error] %s', response_text) 290 | return 291 | if not response_text.startswith(BOUNDARY): 292 | yield escape_unicode(response_text) 293 | return 294 | for packet_text in _yield_text_from_framed_data( 295 | response_text, escape_unicode): 296 | yield packet_text 297 | 298 | def close(self): 299 | _get_response( 300 | self._http_session.get, 301 | self._url, 302 | params=dict(list(self._params.items()) + [('disconnect', True)])) 303 | self._connected = False 304 | 305 | 306 | def _yield_text_from_framed_data(framed_data, parse=lambda x: x): 307 | parts = [parse(x) for x in framed_data.split(BOUNDARY)] 308 | for text_length, text in zip(parts[1::2], parts[2::2]): 309 | if text_length != str(len(text)): 310 | warning = 'invalid declared length=%s for packet_text=%s' % ( 311 | text_length, text) 312 | _log.warn('[packet error] %s', warning) 313 | continue 314 | yield text 315 | 316 | 317 | def _get_response(request, *args, **kw): 318 | try: 319 | response = request(*args, **kw) 320 | except requests.exceptions.Timeout as e: 321 | raise TimeoutError(e) 322 | except requests.exceptions.ConnectionError as e: 323 | raise ConnectionError(e) 324 | except requests.exceptions.SSLError as e: 325 | raise ConnectionError('could not negotiate SSL (%s)' % e) 326 | status = response.status_code 327 | if 200 != status: 328 | raise ConnectionError('unexpected status code (%s)' % status) 329 | return response 330 | 331 | 332 | def _prepare_http_session(kw): 333 | http_session = requests.Session() 334 | http_session.headers.update(kw.get('headers', {})) 335 | http_session.auth = kw.get('auth') 336 | http_session.proxies.update(kw.get('proxies', {})) 337 | http_session.hooks.update(kw.get('hooks', {})) 338 | http_session.params.update(kw.get('params', {})) 339 | http_session.verify = kw.get('verify') 340 | http_session.cert = kw.get('cert') 341 | http_session.cookies.update(kw.get('cookies', {})) 342 | return http_session 343 | -------------------------------------------------------------------------------- /huobi_client/socketIO_client/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import requests 4 | import time 5 | from collections import namedtuple 6 | try: 7 | from urllib.parse import urlparse as parse_url 8 | except ImportError: 9 | from urlparse import urlparse as parse_url 10 | 11 | from .exceptions import ( 12 | SocketIOError, ConnectionError, TimeoutError, PacketError) 13 | from .symmetries import _get_text 14 | from .transports import ( 15 | _get_response, TRANSPORTS, 16 | _WebsocketTransport, _XHR_PollingTransport, _JSONP_PollingTransport) 17 | 18 | 19 | __version__ = '0.5.7' 20 | _SocketIOSession = namedtuple('_SocketIOSession', [ 21 | 'id', 22 | 'heartbeat_timeout', 23 | 'server_supported_transports', 24 | ]) 25 | _log = logging.getLogger(__name__) 26 | PROTOCOL_VERSION = 1 27 | RETRY_INTERVAL_IN_SECONDS = 1 28 | 29 | 30 | class BaseNamespace(object): 31 | 'Define client behavior' 32 | 33 | def __init__(self, _transport, path): 34 | self._transport = _transport 35 | self.path = path 36 | self._was_connected = False 37 | self._callback_by_event = {} 38 | self.initialize() 39 | 40 | def initialize(self): 41 | 'Initialize custom variables here; you can override this method' 42 | pass 43 | 44 | def message(self, data='', callback=None): 45 | self._transport.message(self.path, data, callback) 46 | 47 | def emit(self, event, *args, **kw): 48 | callback, args = find_callback(args, kw) 49 | self._transport.emit(self.path, event, args, callback) 50 | 51 | def disconnect(self): 52 | self._transport.disconnect(self.path) 53 | 54 | def on(self, event, callback): 55 | 'Define a callback to handle a custom event emitted by the server' 56 | self._callback_by_event[event] = callback 57 | 58 | def on_connect(self): 59 | 'Called after server connects; you can override this method' 60 | pass 61 | 62 | def on_disconnect(self): 63 | 'Called after server disconnects; you can override this method' 64 | pass 65 | 66 | def on_heartbeat(self): 67 | 'Called after server sends a heartbeat; you can override this method' 68 | pass 69 | 70 | def on_message(self, data): 71 | 'Called after server sends a message; you can override this method' 72 | pass 73 | 74 | def on_event(self, event, *args): 75 | """ 76 | Called after server sends an event; you can override this method. 77 | Called only if a custom event handler does not exist, 78 | such as one defined by namespace.on('my_event', my_function). 79 | """ 80 | callback, args = find_callback(args) 81 | if callback: 82 | callback(*args) 83 | 84 | def on_error(self, reason, advice): 85 | 'Called after server sends an error; you can override this method' 86 | pass 87 | 88 | def on_noop(self): 89 | 'Called after server sends a noop; you can override this method' 90 | pass 91 | 92 | def on_open(self, *args): 93 | pass 94 | 95 | def on_close(self, *args): 96 | pass 97 | 98 | def on_retry(self, *args): 99 | pass 100 | 101 | def on_reconnect(self, *args): 102 | pass 103 | 104 | def _find_event_callback(self, event): 105 | # Check callbacks defined by on() 106 | try: 107 | return self._callback_by_event[event] 108 | except KeyError: 109 | pass 110 | # Convert connect to reconnect if we have seen connect already 111 | if event == 'connect': 112 | if not self._was_connected: 113 | self._was_connected = True 114 | else: 115 | event = 'reconnect' 116 | # Check callbacks defined explicitly or use on_event() 117 | return getattr( 118 | self, 119 | 'on_' + event.replace(' ', '_'), 120 | lambda *args: self.on_event(event, *args)) 121 | 122 | 123 | class LoggingNamespace(BaseNamespace): 124 | 125 | def _log(self, level, msg, *attrs): 126 | _log.log(level, '%s: %s' % (self._transport._url, msg), *attrs) 127 | 128 | def on_connect(self): 129 | self._log(logging.DEBUG, '%s [connect]', self.path) 130 | super(LoggingNamespace, self).on_connect() 131 | 132 | def on_disconnect(self): 133 | self._log(logging.DEBUG, '%s [disconnect]', self.path) 134 | super(LoggingNamespace, self).on_disconnect() 135 | 136 | def on_heartbeat(self): 137 | self._log(logging.DEBUG, '%s [heartbeat]', self.path) 138 | super(LoggingNamespace, self).on_heartbeat() 139 | 140 | def on_message(self, data): 141 | self._log(logging.INFO, '%s [message] %s', self.path, data) 142 | super(LoggingNamespace, self).on_message(data) 143 | 144 | def on_event(self, event, *args): 145 | callback, args = find_callback(args) 146 | arguments = [repr(_) for _ in args] 147 | if callback: 148 | arguments.append('callback(*args)') 149 | self._log(logging.INFO, '%s [event] %s(%s)', self.path, event, 150 | ', '.join(arguments)) 151 | super(LoggingNamespace, self).on_event(event, *args) 152 | 153 | def on_error(self, reason, advice): 154 | self._log(logging.INFO, '%s [error] %s', self.path, advice) 155 | super(LoggingNamespace, self).on_error(reason, advice) 156 | 157 | def on_noop(self): 158 | self._log(logging.INFO, '%s [noop]', self.path) 159 | super(LoggingNamespace, self).on_noop() 160 | 161 | def on_open(self, *args): 162 | self._log(logging.INFO, '%s [open] %s', self.path, args) 163 | super(LoggingNamespace, self).on_open(*args) 164 | 165 | def on_close(self, *args): 166 | self._log(logging.INFO, '%s [close] %s', self.path, args) 167 | super(LoggingNamespace, self).on_close(*args) 168 | 169 | def on_retry(self, *args): 170 | self._log(logging.INFO, '%s [retry] %s', self.path, args) 171 | super(LoggingNamespace, self).on_retry(*args) 172 | 173 | def on_reconnect(self, *args): 174 | self._log(logging.INFO, '%s [reconnect] %s', self.path, args) 175 | super(LoggingNamespace, self).on_reconnect(*args) 176 | 177 | 178 | class SocketIO(object): 179 | 180 | """Create a socket.io client that connects to a socket.io server 181 | at the specified host and port. 182 | 183 | - Define the behavior of the client by specifying a custom Namespace. 184 | - Prefix host with https:// to use SSL. 185 | - Set wait_for_connection=True to block until we have a connection. 186 | - Specify desired transports=['websocket', 'xhr-polling']. 187 | - Pass query params, headers, cookies, proxies as keyword arguments. 188 | 189 | SocketIO('localhost', 8000, 190 | params={'q': 'qqq'}, 191 | headers={'Authorization': 'Basic ' + b64encode('username:password')}, 192 | cookies={'a': 'aaa'}, 193 | proxies={'https': 'https://proxy.example.com:8080'}) 194 | """ 195 | 196 | def __init__( 197 | self, host, port=None, Namespace=None, 198 | wait_for_connection=True, transports=TRANSPORTS, 199 | resource='socket.io', **kw): 200 | self.is_secure, self._base_url = _parse_host(host, port, resource) 201 | self.wait_for_connection = wait_for_connection 202 | self._namespace_by_path = {} 203 | self._client_supported_transports = transports 204 | self._kw = kw 205 | if Namespace: 206 | self.define(Namespace) 207 | 208 | def _log(self, level, msg, *attrs): 209 | _log.log(level, '%s: %s' % (self._base_url, msg), *attrs) 210 | 211 | def __enter__(self): 212 | return self 213 | 214 | def __exit__(self, *exception_pack): 215 | self.disconnect() 216 | 217 | def __del__(self): 218 | self.disconnect() 219 | 220 | def define(self, Namespace, path=''): 221 | if path: 222 | self._transport.connect(path) 223 | namespace = Namespace(self._transport, path) 224 | self._namespace_by_path[path] = namespace 225 | return namespace 226 | 227 | def on(self, event, callback, path=''): 228 | if path not in self._namespace_by_path: 229 | self.define(BaseNamespace, path) 230 | return self.get_namespace(path).on(event, callback) 231 | 232 | def message(self, data='', callback=None, path=''): 233 | self._transport.message(path, data, callback) 234 | 235 | def emit(self, event, *args, **kw): 236 | path = kw.get('path', '') 237 | callback, args = find_callback(args, kw) 238 | self._transport.emit(path, event, args, callback) 239 | 240 | def wait(self, seconds=None, for_callbacks=False): 241 | """Wait in a loop and process events as defined in the namespaces. 242 | 243 | - Omit seconds, i.e. call wait() without arguments, to wait forever. 244 | """ 245 | warning_screen = _yield_warning_screen(seconds) 246 | timeout = None if seconds is None else min( 247 | self._heartbeat_interval, seconds) 248 | 249 | for elapsed_time in warning_screen: 250 | if self._stop_waiting(for_callbacks): 251 | break 252 | try: 253 | try: 254 | self._process_events(timeout) 255 | except TimeoutError: 256 | pass 257 | next(self._heartbeat_pacemaker) 258 | except ConnectionError as e: 259 | try: 260 | warning = Exception('[connection error] %s' % e) 261 | warning_screen.throw(warning) 262 | except StopIteration: 263 | self._log(logging.WARNING, warning) 264 | try: 265 | namespace = self._namespace_by_path[''] 266 | namespace.on_disconnect() 267 | except KeyError: 268 | pass 269 | 270 | def _process_events(self, timeout=None): 271 | for packet in self._transport.recv_packet(timeout): 272 | try: 273 | self._process_packet(packet) 274 | except PacketError as e: 275 | self._log(logging.WARNING, '[packet error] %s', e) 276 | 277 | def _process_packet(self, packet): 278 | code, packet_id, path, data = packet 279 | namespace = self.get_namespace(path) 280 | delegate = self._get_delegate(code) 281 | delegate(packet, namespace._find_event_callback) 282 | 283 | def _stop_waiting(self, for_callbacks): 284 | # Use __transport to make sure that we do not reconnect inadvertently 285 | if for_callbacks and not self.__transport.has_ack_callback: 286 | return True 287 | if self.__transport._wants_to_disconnect: 288 | return True 289 | return False 290 | 291 | def wait_for_callbacks(self, seconds=None): 292 | self.wait(seconds, for_callbacks=True) 293 | 294 | def disconnect(self, path=''): 295 | try: 296 | self._transport.disconnect(path) 297 | except ReferenceError: 298 | pass 299 | try: 300 | namespace = self._namespace_by_path[path] 301 | namespace.on_disconnect() 302 | del self._namespace_by_path[path] 303 | except KeyError: 304 | pass 305 | 306 | @property 307 | def connected(self): 308 | try: 309 | transport = self.__transport 310 | except AttributeError: 311 | return False 312 | else: 313 | return transport.connected 314 | 315 | @property 316 | def _transport(self): 317 | try: 318 | if self.connected: 319 | return self.__transport 320 | except AttributeError: 321 | pass 322 | socketIO_session = self._get_socketIO_session() 323 | supported_transports = self._get_supported_transports(socketIO_session) 324 | self._heartbeat_pacemaker = self._make_heartbeat_pacemaker( 325 | heartbeat_timeout=socketIO_session.heartbeat_timeout) 326 | next(self._heartbeat_pacemaker) 327 | warning_screen = _yield_warning_screen(seconds=None) 328 | for elapsed_time in warning_screen: 329 | try: 330 | self._transport_name = supported_transports.pop(0) 331 | except IndexError: 332 | raise ConnectionError('Could not negotiate a transport') 333 | try: 334 | self.__transport = self._get_transport( 335 | socketIO_session, self._transport_name) 336 | break 337 | except ConnectionError: 338 | pass 339 | for path, namespace in self._namespace_by_path.items(): 340 | namespace._transport = self.__transport 341 | if path: 342 | self.__transport.connect(path) 343 | return self.__transport 344 | 345 | def _get_socketIO_session(self): 346 | warning_screen = _yield_warning_screen(seconds=None) 347 | for elapsed_time in warning_screen: 348 | try: 349 | return _get_socketIO_session( 350 | self.is_secure, self._base_url, **self._kw) 351 | except ConnectionError as e: 352 | if not self.wait_for_connection: 353 | raise 354 | warning = Exception('[waiting for connection] %s' % e) 355 | try: 356 | warning_screen.throw(warning) 357 | except StopIteration: 358 | self._log(logging.WARNING, warning) 359 | 360 | def _get_supported_transports(self, session): 361 | self._log( 362 | logging.DEBUG, '[transports available] %s', 363 | ' '.join(session.server_supported_transports)) 364 | supported_transports = [ 365 | x for x in self._client_supported_transports if 366 | x in session.server_supported_transports] 367 | if not supported_transports: 368 | raise SocketIOError(' '.join([ 369 | 'could not negotiate a transport:', 370 | 'client supports %s but' % ', '.join( 371 | self._client_supported_transports), 372 | 'server supports %s' % ', '.join( 373 | session.server_supported_transports), 374 | ])) 375 | return supported_transports 376 | 377 | def _get_transport(self, session, transport_name): 378 | self._log(logging.DEBUG, '[transport chosen] %s', transport_name) 379 | return { 380 | 'websocket': _WebsocketTransport, 381 | 'xhr-polling': _XHR_PollingTransport, 382 | 'jsonp-polling': _JSONP_PollingTransport, 383 | }[transport_name](session, self.is_secure, self._base_url, **self._kw) 384 | 385 | def _make_heartbeat_pacemaker(self, heartbeat_timeout): 386 | self._heartbeat_interval = heartbeat_timeout / 2 387 | heartbeat_time = time.time() 388 | while True: 389 | yield 390 | if time.time() - heartbeat_time > self._heartbeat_interval: 391 | heartbeat_time = time.time() 392 | self._transport.send_heartbeat() 393 | 394 | def get_namespace(self, path=''): 395 | try: 396 | return self._namespace_by_path[path] 397 | except KeyError: 398 | raise PacketError('unhandled namespace path (%s)' % path) 399 | 400 | def _get_delegate(self, code): 401 | try: 402 | return { 403 | '0': self._on_disconnect, 404 | '1': self._on_connect, 405 | '2': self._on_heartbeat, 406 | '3': self._on_message, 407 | '4': self._on_json, 408 | '5': self._on_event, 409 | '6': self._on_ack, 410 | '7': self._on_error, 411 | '8': self._on_noop, 412 | }[code] 413 | except KeyError: 414 | raise PacketError('unexpected code (%s)' % code) 415 | 416 | def _on_disconnect(self, packet, find_event_callback): 417 | find_event_callback('disconnect')() 418 | 419 | def _on_connect(self, packet, find_event_callback): 420 | find_event_callback('connect')() 421 | 422 | def _on_heartbeat(self, packet, find_event_callback): 423 | find_event_callback('heartbeat')() 424 | 425 | def _on_message(self, packet, find_event_callback): 426 | code, packet_id, path, data = packet 427 | args = [data] 428 | if packet_id: 429 | args.append(self._prepare_to_send_ack(path, packet_id)) 430 | find_event_callback('message')(*args) 431 | 432 | def _on_json(self, packet, find_event_callback): 433 | code, packet_id, path, data = packet 434 | args = [json.loads(data)] 435 | if packet_id: 436 | args.append(self._prepare_to_send_ack(path, packet_id)) 437 | find_event_callback('message')(*args) 438 | 439 | def _on_event(self, packet, find_event_callback): 440 | code, packet_id, path, data = packet 441 | value_by_name = json.loads(data) 442 | event = value_by_name['name'] 443 | args = value_by_name.get('args', []) 444 | if packet_id: 445 | args.append(self._prepare_to_send_ack(path, packet_id)) 446 | find_event_callback(event)(*args) 447 | 448 | def _on_ack(self, packet, find_event_callback): 449 | code, packet_id, path, data = packet 450 | data_parts = data.split('+', 1) 451 | packet_id = data_parts[0] 452 | try: 453 | ack_callback = self._transport.get_ack_callback(packet_id) 454 | except KeyError: 455 | return 456 | args = json.loads(data_parts[1]) if len(data_parts) > 1 else [] 457 | ack_callback(*args) 458 | 459 | def _on_error(self, packet, find_event_callback): 460 | code, packet_id, path, data = packet 461 | reason, advice = data.split('+', 1) 462 | find_event_callback('error')(reason, advice) 463 | 464 | def _on_noop(self, packet, find_event_callback): 465 | find_event_callback('noop')() 466 | 467 | def _prepare_to_send_ack(self, path, packet_id): 468 | 'Return function that acknowledges the server' 469 | return lambda *args: self._transport.ack(path, packet_id, *args) 470 | 471 | 472 | def find_callback(args, kw=None): 473 | 'Return callback whether passed as a last argument or as a keyword' 474 | if args and callable(args[-1]): 475 | return args[-1], args[:-1] 476 | try: 477 | return kw['callback'], args 478 | except (KeyError, TypeError): 479 | return None, args 480 | 481 | 482 | def _parse_host(host, port, resource): 483 | if not host.startswith('http'): 484 | host = 'http://' + host 485 | url_pack = parse_url(host) 486 | is_secure = url_pack.scheme == 'https' 487 | port = port or url_pack.port or (443 if is_secure else 80) 488 | base_url = '%s:%d%s/%s/%s' % ( 489 | url_pack.hostname, port, url_pack.path, resource, PROTOCOL_VERSION) 490 | return is_secure, base_url 491 | 492 | 493 | def _yield_warning_screen(seconds=None): 494 | last_warning = None 495 | for elapsed_time in _yield_elapsed_time(seconds): 496 | try: 497 | yield elapsed_time 498 | except Exception as warning: 499 | warning = str(warning) 500 | if last_warning != warning: 501 | last_warning = warning 502 | _log.warn(warning) 503 | time.sleep(RETRY_INTERVAL_IN_SECONDS) 504 | 505 | 506 | def _yield_elapsed_time(seconds=None): 507 | start_time = time.time() 508 | if seconds is None: 509 | while True: 510 | yield time.time() - start_time 511 | while time.time() - start_time < seconds: 512 | yield time.time() - start_time 513 | 514 | 515 | def _get_socketIO_session(is_secure, base_url, **kw): 516 | server_url = '%s://%s/' % ('https' if is_secure else 'http', base_url) 517 | try: 518 | response = _get_response(requests.get, server_url, **kw) 519 | except TimeoutError as e: 520 | raise ConnectionError(e) 521 | response_parts = _get_text(response).split(':') 522 | return _SocketIOSession( 523 | id=response_parts[0], 524 | heartbeat_timeout=int(response_parts[1]), 525 | server_supported_transports=response_parts[3].split(',')) 526 | --------------------------------------------------------------------------------