insight-proxy-client 0.0.1__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.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: insight-proxy-client
3
+ Version: 0.0.1
4
+ Summary: Python client for insight_proxy with Insight 6.1.2 compatible data APIs
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3.9
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: pandas<3,>=1.5
14
+ Requires-Dist: numpy<3,>=1.23
15
+
16
+ # insight-proxy-client
17
+
18
+ Python client for the **insight_proxy** TCP service. Data APIs target **Insight 6.1.2**;
19
+ the import namespace is `insight_proxy.com.insight`.
20
+ This is an independent adapter, not the official Insight SDK.
21
+
22
+ ## Installation
23
+
24
+ ```shell
25
+ python -m pip install insight-proxy-client==0.0.1
26
+ ```
27
+
28
+ Supports Python 3.9–3.12. pandas and NumPy are installed as dependencies.
29
+ The client does not require the vendor SDK or native DLLs. A separately deployed
30
+ insight_proxy server with Insight 6.1.2 and valid data access is required.
31
+ This package does not include the server executable or account credentials.
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ from datetime import datetime
37
+ from insight_proxy.com.insight import Client
38
+ from insight_proxy.com.insight.market_service import market_service
39
+
40
+ class Market(market_service):
41
+ def on_subscribe_tick(self, result):
42
+ print(result)
43
+
44
+ def on_query_response(self, result):
45
+ print(result)
46
+
47
+ # Both Client() and Client(Market()) are supported.
48
+ # Defaults: host="127.0.0.1", port=8765, token="123456".
49
+ # The token must match the server configuration.
50
+ with Client(Market()) as client:
51
+ frame = client.query.get_kline(
52
+ "601688.SH",
53
+ [datetime(2026, 10, 19), datetime(2026, 10, 26, 23, 59, 59)],
54
+ frequency="daily", fq="none",
55
+ )
56
+ print(frame)
57
+ client.subscribe.subscribe_tick_by_id(["601688.SH"], mode="coverage")
58
+ input("Press Enter to close the subscription session.\n")
59
+ ```
60
+
61
+ Construction does not open a connection. Use `client.connect()` / `client.close()`
62
+ or a `with` block. Override connection settings using keyword arguments:
63
+ `Client(Market(), host="127.0.0.1", port=8765, token="your-server-token")`.
64
+ Example dates are fixed historical values; select dates and instruments available
65
+ to your server account.
66
+
67
+ ## Migrating SDK data calls
68
+
69
+ Replace `insight_python.com.insight` imports with `insight_proxy.com.insight`.
70
+ Replace SDK login and initialization with the proxy connection:
71
+
72
+ ```python
73
+ from insight_proxy.com.insight import connect, close, query, subscribe, playback
74
+
75
+ connect() # or connect(Market(), host="...", port=8765, token="...")
76
+ try:
77
+ result = query.get_kline() # retains the SDK's missing-argument response
78
+ finally:
79
+ close()
80
+ ```
81
+
82
+ The compatibility layer exposes 80 query functions, 11 data subscription functions,
83
+ local `subscribe.sync()`, two playback functions and the `market_service` callbacks.
84
+ It preserves Insight 6.1.2 data function signatures, DataFrame/Series values,
85
+ column ordering, indexes, dtypes, datetime values, tuples and callback names.
86
+ SDK login/logging helpers and native gateway types are outside this client API.
87
+ One compatibility session exclusively owns the server's native SDK login.
88
+ Native SDK permissions and availability still determine which requests succeed.
89
+
90
+ Do not install the older combined `insight_proxy` distribution alongside this
91
+ client package in the same environment: they provide the same import namespace.
@@ -0,0 +1,76 @@
1
+ # insight-proxy-client
2
+
3
+ Python client for the **insight_proxy** TCP service. Data APIs target **Insight 6.1.2**;
4
+ the import namespace is `insight_proxy.com.insight`.
5
+ This is an independent adapter, not the official Insight SDK.
6
+
7
+ ## Installation
8
+
9
+ ```shell
10
+ python -m pip install insight-proxy-client==0.0.1
11
+ ```
12
+
13
+ Supports Python 3.9–3.12. pandas and NumPy are installed as dependencies.
14
+ The client does not require the vendor SDK or native DLLs. A separately deployed
15
+ insight_proxy server with Insight 6.1.2 and valid data access is required.
16
+ This package does not include the server executable or account credentials.
17
+
18
+ ## Quick start
19
+
20
+ ```python
21
+ from datetime import datetime
22
+ from insight_proxy.com.insight import Client
23
+ from insight_proxy.com.insight.market_service import market_service
24
+
25
+ class Market(market_service):
26
+ def on_subscribe_tick(self, result):
27
+ print(result)
28
+
29
+ def on_query_response(self, result):
30
+ print(result)
31
+
32
+ # Both Client() and Client(Market()) are supported.
33
+ # Defaults: host="127.0.0.1", port=8765, token="123456".
34
+ # The token must match the server configuration.
35
+ with Client(Market()) as client:
36
+ frame = client.query.get_kline(
37
+ "601688.SH",
38
+ [datetime(2026, 10, 19), datetime(2026, 10, 26, 23, 59, 59)],
39
+ frequency="daily", fq="none",
40
+ )
41
+ print(frame)
42
+ client.subscribe.subscribe_tick_by_id(["601688.SH"], mode="coverage")
43
+ input("Press Enter to close the subscription session.\n")
44
+ ```
45
+
46
+ Construction does not open a connection. Use `client.connect()` / `client.close()`
47
+ or a `with` block. Override connection settings using keyword arguments:
48
+ `Client(Market(), host="127.0.0.1", port=8765, token="your-server-token")`.
49
+ Example dates are fixed historical values; select dates and instruments available
50
+ to your server account.
51
+
52
+ ## Migrating SDK data calls
53
+
54
+ Replace `insight_python.com.insight` imports with `insight_proxy.com.insight`.
55
+ Replace SDK login and initialization with the proxy connection:
56
+
57
+ ```python
58
+ from insight_proxy.com.insight import connect, close, query, subscribe, playback
59
+
60
+ connect() # or connect(Market(), host="...", port=8765, token="...")
61
+ try:
62
+ result = query.get_kline() # retains the SDK's missing-argument response
63
+ finally:
64
+ close()
65
+ ```
66
+
67
+ The compatibility layer exposes 80 query functions, 11 data subscription functions,
68
+ local `subscribe.sync()`, two playback functions and the `market_service` callbacks.
69
+ It preserves Insight 6.1.2 data function signatures, DataFrame/Series values,
70
+ column ordering, indexes, dtypes, datetime values, tuples and callback names.
71
+ SDK login/logging helpers and native gateway types are outside this client API.
72
+ One compatibility session exclusively owns the server's native SDK login.
73
+ Native SDK permissions and availability still determine which requests succeed.
74
+
75
+ Do not install the older combined `insight_proxy` distribution alongside this
76
+ client package in the same environment: they provide the same import namespace.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "insight-proxy-client"
7
+ version = "0.0.1"
8
+ description = "Python client for insight_proxy with Insight 6.1.2 compatible data APIs"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = ["pandas>=1.5,<3", "numpy>=1.23,<3"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.9",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+ include = ["insight_proxy*"]
24
+
25
+ [tool.setuptools]
26
+ package-dir = {"" = "src"}
27
+ include-package-data = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """No vendor SDK is imported on the client side."""
2
+ from .client import InsightClient, BridgeError
3
+
4
+ __all__ = ["InsightClient", "BridgeError"]
@@ -0,0 +1,183 @@
1
+ """Thread-safe request/response multiplexing with a bounded event queue."""
2
+ import itertools
3
+ import queue
4
+ import socket
5
+ import ssl
6
+ import threading
7
+
8
+ from .protocol import frame_size, pack, unpack, restore_dates
9
+
10
+
11
+ class BridgeError(RuntimeError):
12
+ def __init__(self, code, message):
13
+ self.code = code
14
+ super().__init__(f"{code}: {message}")
15
+
16
+
17
+ class InsightClient:
18
+ def __init__(self, host="127.0.0.1", port=8765, *, token="", timeout=30,
19
+ event_queue_size=10000, ssl_context=None):
20
+ self.host, self.port = host, port
21
+ self.token, self.timeout = token, timeout
22
+ self.ssl_context = ssl_context
23
+ self._sock = None
24
+ self._reader = None
25
+ self._send_lock = threading.Lock()
26
+ self._lock = threading.Lock()
27
+ self._ids = itertools.count(1)
28
+ self._pending = {}
29
+ self._events = queue.Queue(maxsize=event_queue_size)
30
+ self._failure = None
31
+
32
+ def connect(self):
33
+ if self._sock is not None:
34
+ raise RuntimeError("Already connected; create a new client for reconnect")
35
+ sock = socket.create_connection((self.host, self.port), self.timeout)
36
+ try:
37
+ if self.ssl_context:
38
+ sock = self.ssl_context.wrap_socket(sock, server_hostname=self.host)
39
+ sock.settimeout(None)
40
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
41
+ self._sock = sock
42
+ self._reader = threading.Thread(target=self._read_loop, daemon=True)
43
+ self._reader.start()
44
+ self.request("hello", {"version": 1, "token": self.token})
45
+ return self
46
+ except BaseException:
47
+ sock.close()
48
+ self.close()
49
+ raise
50
+
51
+ def _exact(self, size):
52
+ chunks = bytearray()
53
+ while len(chunks) < size:
54
+ chunk = self._sock.recv(size - len(chunks))
55
+ if not chunk:
56
+ raise ConnectionError("Bridge disconnected; reconnect and resubscribe")
57
+ chunks.extend(chunk)
58
+ return bytes(chunks)
59
+
60
+ def _fail(self, error):
61
+ with self._lock:
62
+ if self._failure is None:
63
+ self._failure = error
64
+ pending, self._pending = self._pending, {}
65
+ for waiter in pending.values():
66
+ waiter.put(self._failure)
67
+ if self._sock:
68
+ try:
69
+ self._sock.shutdown(socket.SHUT_RDWR)
70
+ except OSError:
71
+ pass
72
+ self._sock.close()
73
+
74
+ def _read_loop(self):
75
+ try:
76
+ while True:
77
+ message = restore_dates(unpack(self._exact(frame_size(self._exact(4)))))
78
+ if message.get("type") == "response":
79
+ with self._lock:
80
+ waiter = self._pending.pop(message.get("id"), None)
81
+ if waiter:
82
+ waiter.put(message)
83
+ elif message.get("type") == "event":
84
+ try:
85
+ self._events.put_nowait(message)
86
+ except queue.Full:
87
+ raise BridgeError("slow_consumer", "Client event queue full; connection closed to signal a data gap")
88
+ else:
89
+ raise ValueError("Unknown server message")
90
+ except Exception as exc:
91
+ self._fail(exc)
92
+
93
+ def request(self, method, params=None, *, timeout=None):
94
+ request_id = next(self._ids)
95
+ waiter = queue.Queue()
96
+ frame = pack({"id": request_id, "method": method, "params": params or {}})
97
+ with self._lock:
98
+ if self._failure:
99
+ raise self._failure
100
+ if self._sock is None:
101
+ raise ConnectionError("Call connect() first")
102
+ self._pending[request_id] = waiter
103
+ try:
104
+ with self._send_lock:
105
+ self._sock.sendall(frame)
106
+ try:
107
+ reply = waiter.get(timeout=self.timeout if timeout is None else timeout)
108
+ except queue.Empty:
109
+ raise TimeoutError(f"{method} timed out; upstream execution may still finish") from None
110
+ if isinstance(reply, Exception):
111
+ raise reply
112
+ if not reply.get("ok"):
113
+ error = reply["error"]
114
+ raise BridgeError(error["code"], error["message"])
115
+ return reply.get("result")
116
+ finally:
117
+ with self._lock:
118
+ self._pending.pop(request_id, None)
119
+
120
+ def ping(self):
121
+ return self.request("ping")
122
+
123
+ def status(self):
124
+ return self.request("status")
125
+
126
+ def methods(self):
127
+ return self.request("methods")
128
+
129
+ def query(self, name, **params):
130
+ """Returns {columns, records, dtypes}; datetime parameters are encoded automatically."""
131
+ return self.request("query", {"name": name, "kwargs": params})
132
+
133
+ def snapshot(self, symbols, datatype):
134
+ return self.query("get_gdp_recent_snap", htsc_code=symbols, data_type=datatype)
135
+
136
+ def static(self, symbols):
137
+ return self.query("get_gdp_static_by_id", htsc_code=symbols)
138
+
139
+ def subscribe(self, datatype, symbols=None):
140
+ """symbols=None subscribes the entire datatype. Returns submitted, not confirmed."""
141
+ return self.request("subscribe", {"datatype": datatype, "symbols": symbols})
142
+
143
+ def unsubscribe(self, datatype, symbols=None):
144
+ return self.request("unsubscribe", {"datatype": datatype, "symbols": symbols})
145
+
146
+ def subscribe_tick(self, symbols):
147
+ """Legacy SDK stock/futures/ETF tick subscription with explicit codes."""
148
+ return self.request("subscribe_tick", {"symbols": symbols})
149
+
150
+ def unsubscribe_tick(self, symbols):
151
+ return self.request("unsubscribe_tick", {"symbols": symbols})
152
+
153
+ def unsubscribe_all(self):
154
+ return self.request("unsubscribe_all")
155
+
156
+ def next_event(self, timeout=None):
157
+ # Check failure first: do not silently serve buffered stale data after a gap.
158
+ import time
159
+ deadline = None if timeout is None else time.monotonic() + timeout
160
+ while True:
161
+ if self._failure:
162
+ raise self._failure
163
+ wait = 0.2 if deadline is None else max(0, min(0.2, deadline - time.monotonic()))
164
+ try:
165
+ return self._events.get(timeout=wait)
166
+ except queue.Empty:
167
+ if deadline is not None and time.monotonic() >= deadline:
168
+ raise TimeoutError("No event received") from None
169
+
170
+ def events(self):
171
+ while True:
172
+ yield self.next_event()
173
+
174
+ def close(self):
175
+ self._fail(ConnectionError("Client closed"))
176
+ if self._reader and threading.current_thread() is not self._reader:
177
+ self._reader.join(timeout=2)
178
+
179
+ def __enter__(self):
180
+ return self.connect()
181
+
182
+ def __exit__(self, *_):
183
+ self.close()
@@ -0,0 +1 @@
1
+ """Insight-compatible module namespace."""
@@ -0,0 +1,9 @@
1
+ """Insight 6.1.2 data API over TCP, for Python 3.9-3.12.
2
+
3
+ Change SDK imports to this package and replace login/initialization with connect().
4
+ An SDK-compatible connection exclusively owns the server's native login session.
5
+ """
6
+ from ._session import Client, connect, close, SDKError
7
+ from . import query, subscribe, playback, market_service
8
+
9
+ __all__ = ['Client', 'connect', 'close', 'SDKError', 'query', 'subscribe', 'playback', 'market_service']
@@ -0,0 +1,2 @@
1
+ """Pinned public API contract, generated from SDK 6.1.2."""
2
+ CONTRACT = {'sdk_version': '6.1.2', 'modules': {'query': {'query_fin_info': '(query_type, params)', 'get_kline': "(htsc_code=None, time=None, frequency='daily', fq='pre')", 'get_derived': '(htsc_code=None, trading_day=None, type=None)', 'get_trade_distribution': '(htsc_code=None, trading_day=None)', 'get_chip_distribution': '(htsc_code=None, trading_day=None)', 'get_money_flow': '(htsc_code=None, trading_day=None)', 'get_change_summary': '(market=None, trading_day=None)', 'get_billboard': '(type=None, market=None)', 'get_stock_info': '(htsc_code=None, listing_date=None, listing_state=None)', 'get_all_stocks_info': '(listing_date=None, exchange=None, listing_state=None)', 'get_trading_days': '(exchange=None, trading_day=None, count=None)', 'get_industries': '(classified=None)', 'get_industry': "(htsc_code=None, classified='sw')", 'get_industry_stocks': "(industry_code=None, classified='sw')", 'get_new_share': '(htsc_code=None, book_start_date_online=None, listing_date=None)', 'get_daily_basic': '(htsc_code=None, trading_day=None)', 'get_stock_valuation': '(htsc_code=None, trading_day=None)', 'get_bond_info': '(htsc_code=None, secu_category_code=None, listing_date=None, issue_start_date=None, end_date=None)', 'get_all_bonds': '(exchange=None, secu_category_code=None, listing_date=None, issue_start_date=None, end_date=None)', 'get_repo_price': '(htsc_code=None, exchange=None, trading_day=None)', 'get_new_con_bond': '(htsc_code=None, exchange=None, book_start_date_online=None, listing_date=None, issue_date=None, convert_code=None)', 'get_bond_price': '(htsc_code=None, exchange=None, trading_day=None)', 'get_con_bond_redemption': '(htsc_code=None, exchange=None, register_date=None)', 'get_con_bond_2_shares_change': '(htsc_code=None, exchange=None, pub_date=None, convert_code=None)', 'get_con_bond_2_shares': '(htsc_code=None, pub_date=None, exer_begin_date=None, exer_end_date=None, convert_code=None, exchange=None)', 'get_income_statement': '(htsc_code=None, end_date=None, period=None)', 'get_balance_sheet': '(htsc_code=None, end_date=None, period=None)', 'get_cashflow_statement': '(htsc_code=None, end_date=None, period=None)', 'get_fin_indicator': '(htsc_code=None, end_date=None, period=None)', 'get_newest_stock_value': '(htsc_code=None, exchange=None)', 'get_company_info': '(htsc_code=None, name=None)', 'get_shareholder_num': '(htsc_code=None, name=None, end_date=None)', 'get_additional_share': '(htsc_code=None, listing_date=None)', 'get_allotment_share': '(htsc_code=None, ini_pub_date=None, is_allot_half_year=None)', 'get_capital_structure': '(htsc_code=None, end_date=None)', 'get_dividend': '(htsc_code=None, right_reg_date=None, ex_divi_date=None, divi_pay_date=None)', 'get_north_bound': '(htsc_code=None, trading_day=None)', 'get_margin_target': '(htsc_code=None, exchange=None)', 'get_margin_summary': '(htsc_code=None, trading_day=None)', 'get_margin_detail': '(exchange=None, trading_day=None)', 'get_shareholders_top10': '(htsc_code=None, change_date=None)', 'get_shareholders_floating_top10': '(htsc_code=None, change_date=None)', 'get_tick': '(htsc_code=None, security_type=None, trading_day=None)', 'get_adj_factor': '(htsc_code=None, begin_date=None)', 'get_locked_shares': '(htsc_code=None, listing_date=None)', 'get_frozen_shares': '(htsc_code=None, freezing_start_date=None)', 'get_hk_industry': '(htsc_code=None)', 'get_hk_industry_stocks': '(industry_code=None)', 'get_hk_daily_basic': '(htsc_code=None, trading_day=None)', 'get_hk_stock_valuation': '(htsc_code=None, trading_day=None)', 'get_hk_stock_basic_info': '(htsc_code=None, listing_date=None, listing_state=None)', 'get_hk_dividend': '(htsc_code=None, ex_divi_date=None)', 'get_hk_index_mapping': '(month=None)', 'get_main_product_info': '(htsc_code=None, product_code=None, product_level=None)', 'get_htsc_margin_target': '()', 'get_htsc_margin_by_id': '(htsc_code=None, data_type=None)', 'get_htsc_margin_by_type': '(data_type=None, security_type=None, is_async=False)', 'get_index_info': '(htsc_code=None, trading_day=None)', 'get_all_index': '(exchange=None, trading_day=None)', 'get_index_component': '(htsc_code=None, name=None, stock_code=None, trading_day=None)', 'get_index_component_pro': '(htsc_code=None, name=None, stock_code=None, trading_day=None)', 'get_factors': '(htsc_code=None, factor_name=None, trading_day=None)', 'get_fund_info': '(htsc_code=None, trading_day=None)', 'get_fund_target': '(htsc_code=None, exchange=None, end_date=None)', 'get_etf_component': '(htsc_code=None, pub_date=None, trading_day=None)', 'get_public_fund_portfolio': '(htsc_code=None, name=None, exchange=None, end_date=None)', 'get_etf_redemption': '(htsc_code=None, exchange=None, trading_day=None)', 'get_basic_info': '(htsc_code=None)', 'get_all_basic_info': '(security_type=None, exchange=None, today=True)', 'get_news': '(htsc_code=None, event_ids=None, pub_date=None)', 'find_edb_index': '(name=None)', 'edb': '(indexes=None, pub_date=None)', 'get_rpt_basicinfo_ht': '(id=None, report_code=None, time=None, title=None, language=None, is_valid=None)', 'get_rpt_stk_ht': '(report_code=None)', 'get_rpt_industry_ht': '(report_code=None)', 'get_rpt_author_ht': '(report_code=None)', 'get_rpt_annex_ht': '(report_code=None)', 'get_rpt_stkpredict_ht': '(report_code=None)', 'get_etf_info': '(query)', 'query_mdcontant': '(security_idsource_and_types, security_id_list)'}, 'subscribe': {'subscribe_tick_by_id': "(htsc_code, mode='coverage')", 'subscribe_tick_by_type': "(query=None, mode='coverage')", 'subscribe_kline_by_id': "(htsc_code=None, frequency=None, mode='coverage')", 'subscribe_kline_by_type': "(query=None, frequency=None, mode='coverage')", 'subscribe_trans_and_order_by_id': "(htsc_code=None, mode='coverage')", 'subscribe_trans_and_order_by_type': "(query=None, mode='coverage')", 'subscribe_htsc_margin_by_id': "(htsc_code=None, data_type=None, mode='coverage')", 'subscribe_htsc_margin_by_type': "(security_type=None, data_type=None, mode='coverage')", 'subscribe_derived': "(type=None, htsc_code=None, exchange=None, frequency='1min', mode='coverage')", 'subscribe_news_by_type': "(query=None, mode='coverage')", 'subscribe_news_by_id': "(htsc_code=None, mode='coverage')", 'sync': '()'}, 'playback': {'playback_tick': "(htsc_code=None, replay_time=None, fq='pre', timeout=600)", 'playback_trans_and_order': "(htsc_code=None, replay_time=None, fq='pre', timeout=600)"}}, 'sha256': {'query': '17976b193d25e5a05e8e3492b9f04ad54a93ddd1a700a30212b721cd09e3b4a7', 'subscribe': 'b7920962f688aac4cb8bedd8c5a60c80846a729b347eafc94719c5faba9913b0', 'playback': 'fdd2d88a9a67f564eae0c6a048916a4c25c0e1ca2bcd38ec03d24a3a5e2e2ddf'}, 'callbacks': ['on_subscribe_tick', 'on_subscribe_kline', 'on_subscribe_trans_and_order', 'on_subscribe_derived', 'on_subscribe_htsc_margin', 'on_subscribe_news', 'on_playback_tick', 'on_playback_trans_and_order', 'onPlaybackStatus', 'onPlaybackResponse', 'onPlaybackControlResponse', 'on_query_response', 'on_query_margin_response']}
@@ -0,0 +1,179 @@
1
+ import builtins
2
+ import functools
3
+ import importlib
4
+ import inspect
5
+ import logging
6
+ import queue
7
+ import threading
8
+
9
+ from ...client import InsightClient, BridgeError
10
+ from ...typed_codec import encode, decode
11
+ from ._contract import CONTRACT
12
+ from .market_service import market_service
13
+
14
+ LOG = logging.getLogger('insight_proxy.compat')
15
+ _default = None
16
+ _default_lock = threading.RLock()
17
+ _EXCEPTIONS = {name: getattr(builtins, name) for name in (
18
+ 'ValueError', 'TypeError', 'KeyError', 'IndexError', 'RuntimeError', 'AttributeError',
19
+ 'NotImplementedError', 'AssertionError', 'ZeroDivisionError', 'OverflowError',
20
+ 'NameError', 'UnboundLocalError', 'TimeoutError', 'OSError', 'ConnectionError',
21
+ 'PermissionError', 'FileNotFoundError', 'ImportError', 'ModuleNotFoundError')}
22
+
23
+
24
+ class SDKError(RuntimeError):
25
+ """A vendor exception whose class is not part of the safe builtin allowlist."""
26
+
27
+
28
+ class _Namespace:
29
+ def __init__(self, client, module):
30
+ self._client, self._module = client, module
31
+
32
+ def __dir__(self):
33
+ return list(CONTRACT['modules'][self._module])
34
+
35
+ def __getattr__(self, name):
36
+ if name not in CONTRACT['modules'][self._module]:
37
+ raise AttributeError(name)
38
+ template = getattr(importlib.import_module('.' + self._module, __package__), name)
39
+ if name == 'sync':
40
+ return template
41
+ signature = inspect.signature(template)
42
+
43
+ @functools.wraps(template)
44
+ def invoke(*args, **kwargs):
45
+ bound = signature.bind(*args, **kwargs)
46
+ bound.apply_defaults()
47
+ return self._client._call(self._module, name, dict(bound.arguments))
48
+ return invoke
49
+
50
+
51
+ class Client:
52
+ """Explicit session alternative to module-level connect/query/subscribe.
53
+
54
+ Callback exceptions are logged and placed in errors. Transport failures are
55
+ also placed there and close the session; neither is silently swallowed.
56
+ on_event receives bridge status/ack/error events, outside the SDK callback API.
57
+ """
58
+ def __init__(self, marketservice=None, *, host='127.0.0.1', port=8765, token='123456',
59
+ timeout=30, ssl_context=None, event_queue_size=10000, on_event=None):
60
+ self.transport = InsightClient(host, port, token=token, timeout=timeout,
61
+ ssl_context=ssl_context, event_queue_size=event_queue_size)
62
+ self.marketservice = market_service() if marketservice is None else marketservice
63
+ self.on_event = on_event
64
+ self.errors = queue.Queue(maxsize=100)
65
+ self.query = _Namespace(self, 'query')
66
+ self.subscribe = _Namespace(self, 'subscribe')
67
+ self.playback = _Namespace(self, 'playback')
68
+ self._closed = threading.Event()
69
+ self._thread = None
70
+ self._connected = False
71
+ self.capabilities = None
72
+
73
+ def connect(self):
74
+ # Fail early with an actionable dependency message, before claiming a login.
75
+ try:
76
+ import pandas # noqa: F401
77
+ except ImportError as exc:
78
+ raise ImportError('Install insight-proxy-client (pandas >=1.5,<3)') from exc
79
+ try:
80
+ self.transport.connect()
81
+ self.capabilities = self.transport.request('sdk_open', {'sdk_version': '6.1.2', 'codec': 1})
82
+ if self.capabilities.get('modules') != CONTRACT['modules']:
83
+ raise BridgeError('incompatible_sdk', 'Server API contract differs from Insight 6.1.2')
84
+ self._connected = True
85
+ self._thread = threading.Thread(target=self._callbacks, name='insight-callbacks', daemon=True)
86
+ self._thread.start()
87
+ return self
88
+ except BaseException:
89
+ self.close()
90
+ raise
91
+
92
+ def _call(self, module, name, kwargs):
93
+ if not self._connected or self._closed.is_set():
94
+ raise ConnectionError('Call connect() first; create a new Client after close()')
95
+ timeout = self.transport.timeout
96
+ # Native playback is blocking and has its own public timeout argument.
97
+ if module == 'playback' and isinstance(kwargs.get('timeout'), (int, float)):
98
+ timeout = max(timeout, kwargs['timeout'] + 15)
99
+ result = self.transport.request('sdk_call', {'module': module, 'name': name,
100
+ 'kwargs': encode(kwargs)}, timeout=timeout)
101
+ if 'exception' in result:
102
+ error = result['exception']
103
+ cls = _EXCEPTIONS.get(error['type'], SDKError)
104
+ raise cls(*decode(error['args']))
105
+ return decode(result['value'])
106
+
107
+ def _record_error(self, error):
108
+ try:
109
+ self.errors.put_nowait(error)
110
+ except queue.Full:
111
+ self.close()
112
+
113
+ def _callbacks(self):
114
+ while not self._closed.is_set():
115
+ try:
116
+ event = self.transport.next_event(timeout=0.2)
117
+ except TimeoutError:
118
+ continue
119
+ except Exception as exc:
120
+ if not self._closed.is_set():
121
+ self._record_error(exc)
122
+ LOG.error('Insight connection ended: %s', exc)
123
+ self.close()
124
+ break
125
+ try:
126
+ if event['event'] == 'sdk_callback':
127
+ name = event['data']['name']
128
+ if name not in CONTRACT['callbacks']:
129
+ raise ValueError('Unknown SDK callback: ' + str(name))
130
+ callback = getattr(self.marketservice, name, None)
131
+ if callback is not None:
132
+ callback(decode(event['data']['value']))
133
+ elif event['event'] == 'sdk_callback_error':
134
+ raise SDKError(event['data']['message'])
135
+ elif self.on_event:
136
+ self.on_event(event)
137
+ except Exception as exc:
138
+ self._record_error(exc)
139
+ LOG.exception('Insight callback failed')
140
+
141
+ def close(self):
142
+ self._closed.set()
143
+ self._connected = False
144
+ self.transport.close()
145
+ if self._thread and threading.current_thread() is not self._thread:
146
+ self._thread.join(timeout=2)
147
+ # The server resets the native login before another compatibility lease.
148
+
149
+ def __enter__(self):
150
+ return self.connect()
151
+
152
+ def __exit__(self, *_):
153
+ self.close()
154
+
155
+
156
+ def connect(marketservice=None, **connection):
157
+ """Initialize module-level APIs; credentials remain in the server config."""
158
+ global _default
159
+ with _default_lock:
160
+ if _default is not None and not _default._closed.is_set():
161
+ raise RuntimeError('A default connection is already active; call close() first')
162
+ _default = Client(marketservice, **connection).connect()
163
+ return _default
164
+
165
+
166
+ def close():
167
+ global _default
168
+ with _default_lock:
169
+ if _default is not None:
170
+ _default.close()
171
+ _default = None
172
+
173
+
174
+ def call(module, name, kwargs):
175
+ with _default_lock:
176
+ client = _default
177
+ if client is None:
178
+ raise ConnectionError('Call insight_proxy.com.insight.connect() first')
179
+ return client._call(module, name, kwargs)
@@ -0,0 +1,43 @@
1
+ """Override these callbacks exactly as with Insight 6.1.2."""
2
+ class market_service:
3
+ def __init__(self):
4
+ pass
5
+
6
+ def on_subscribe_tick(self, result):
7
+ pass
8
+
9
+ def on_subscribe_kline(self, result):
10
+ pass
11
+
12
+ def on_subscribe_trans_and_order(self, result):
13
+ pass
14
+
15
+ def on_subscribe_derived(self, result):
16
+ pass
17
+
18
+ def on_subscribe_htsc_margin(self, result):
19
+ pass
20
+
21
+ def on_subscribe_news(self, result):
22
+ pass
23
+
24
+ def on_playback_tick(self, result):
25
+ pass
26
+
27
+ def on_playback_trans_and_order(self, result):
28
+ pass
29
+
30
+ def onPlaybackStatus(self, status):
31
+ pass
32
+
33
+ def onPlaybackResponse(self, response):
34
+ pass
35
+
36
+ def onPlaybackControlResponse(self, response):
37
+ pass
38
+
39
+ def on_query_response(self, result):
40
+ pass
41
+
42
+ def on_query_margin_response(self, result):
43
+ pass
@@ -0,0 +1,13 @@
1
+ """Insight 6.1.2 signatures. Generated by tools/generate-compat.py."""
2
+ from ._session import call as _call
3
+
4
+
5
+ def playback_tick(htsc_code=None, replay_time=None, fq='pre', timeout=600):
6
+ """Socket equivalent of Insight 6.1.2 playback.playback_tick."""
7
+ return _call('playback', 'playback_tick', locals())
8
+
9
+ def playback_trans_and_order(htsc_code=None, replay_time=None, fq='pre', timeout=600):
10
+ """Socket equivalent of Insight 6.1.2 playback.playback_trans_and_order."""
11
+ return _call('playback', 'playback_trans_and_order', locals())
12
+
13
+ __all__ = ['playback_tick', 'playback_trans_and_order']