tsshare 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tsshare/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """tsshare SDK
2
+
3
+ 用法示例(tinyshare 风格,只需改一行代码):
4
+
5
+ import tsshare as ts
6
+ ts.set_token("your-auth-code")
7
+ pro = ts.pro_api()
8
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20240201")
9
+
10
+ 也支持显式传参风格:
11
+
12
+ import tsshare as ts
13
+ pro = ts.pro_api("your-auth-code", base_url="http://localhost:8000")
14
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20240201")
15
+ """
16
+
17
+ from .client import MyShareClient, pro_api, set_token, set_base_url
18
+
19
+ __all__ = ["MyShareClient", "pro_api", "set_token", "set_base_url"]
tsshare/client.py ADDED
@@ -0,0 +1,417 @@
1
+ """myshare 客户端 SDK
2
+
3
+ 提供与 tushare.pro_api 类似的调用方式:
4
+
5
+ import myshare as ts
6
+ ts.set_token("your-auth-code")
7
+ pro = ts.pro_api()
8
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20240201")
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import base64 as _b64
13
+ import hashlib
14
+ import json
15
+ import logging
16
+ import os
17
+ import platform
18
+ import subprocess
19
+ from pathlib import Path
20
+ from typing import Any, Callable, Optional
21
+
22
+ import requests
23
+ from requests.adapters import HTTPAdapter
24
+ from urllib3.util.retry import Retry
25
+ import pandas as pd
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # -------------------- 默认网关(混淆存储,防止字符串搜索直接提取)--------------------
30
+ # 以下常量是 base64 编码的默认服务器地址,运行时解码使用。
31
+ # 修改说明:将你的真实服务器地址做 base64 编码后替换下方常量即可:
32
+ # python -c "import base64; print(base64.b64encode(b'http://YOUR_IP:PORT').decode())"
33
+ _E = b"aHR0cHM6Ly80Ny4xMTIuMTkxLjc1" # base64("https://47.112.191.75")
34
+
35
+
36
+ def _default_base_url() -> str:
37
+ """运行时解码默认网关地址。"""
38
+ return _b64.b64decode(_E).decode("utf-8")
39
+
40
+
41
+ # -------------------- 全局配置(兼容 tinyshare 风格) --------------------
42
+ _global_auth_code: Optional[str] = None
43
+ _global_base_url: Optional[str] = None
44
+
45
+
46
+ def set_token(token: str) -> None:
47
+ """全局设置 myshare auth_code(兼容 tinyshare 的 ts.set_token() 调用风格)
48
+
49
+ 示例:
50
+ import myshare as ts
51
+ ts.set_token("msu_xxx")
52
+ pro = ts.pro_api()
53
+ """
54
+ global _global_auth_code
55
+ _global_auth_code = token
56
+ logger.debug("全局 auth_code 已设置")
57
+
58
+
59
+ def set_base_url(url: str) -> None:
60
+ """全局设置 myshare 网关地址(可选,覆盖内置默认值)
61
+
62
+ 示例:
63
+ ts.set_base_url("http://192.168.1.100:8000")
64
+ """
65
+ global _global_base_url
66
+ _global_base_url = url
67
+ logger.debug("全局 base_url 已设置")
68
+
69
+
70
+ # -------------------- 设备指纹采集 --------------------
71
+
72
+ def _get_device_id() -> str:
73
+ """获取本机设备唯一 ID,并缓存到 ~/.myshare/device_id.json。
74
+
75
+ 采集逻辑:
76
+ - Windows : PowerShell Get-CimInstance 读取 CPU ProcessorId + 硬盘序列号 → SHA256
77
+ - macOS : system_profiler 读取 Hardware UUID → SHA256
78
+ - Linux : 读取 /etc/machine-id 或 /var/lib/dbus/machine-id → SHA256
79
+
80
+ 如果所有方式都失败,退化为 hostname + platform 信息的 SHA256。
81
+ """
82
+ cache_dir = Path.home() / ".myshare"
83
+ cache_file = cache_dir / "device_id.json"
84
+
85
+ # 优先读取已缓存的 ID,并校验格式(SHA256 = 64位十六进制)
86
+ if cache_file.exists():
87
+ try:
88
+ data = json.loads(cache_file.read_text(encoding="utf-8"))
89
+ device_id = data.get("device_id", "")
90
+ if (
91
+ device_id
92
+ and len(device_id) == 64
93
+ and all(c in "0123456789abcdef" for c in device_id)
94
+ ):
95
+ return device_id
96
+ except Exception:
97
+ pass
98
+
99
+ raw_id = _collect_hardware_fingerprint()
100
+ device_id = hashlib.sha256(raw_id.encode("utf-8")).hexdigest()
101
+
102
+ # 持久化缓存
103
+ try:
104
+ cache_dir.mkdir(parents=True, exist_ok=True)
105
+ cache_file.write_text(
106
+ json.dumps({"device_id": device_id}, ensure_ascii=False),
107
+ encoding="utf-8",
108
+ )
109
+ except Exception as e:
110
+ logger.debug("写入设备指纹缓存失败: %s", e)
111
+
112
+ logger.debug("设备 ID 已生成: %s...", device_id[:8])
113
+ return device_id
114
+
115
+
116
+ def _collect_hardware_fingerprint() -> str:
117
+ """根据操作系统收集硬件指纹原始字符串。"""
118
+ system = platform.system()
119
+
120
+ if system == "Windows":
121
+ return _fingerprint_windows()
122
+ elif system == "Darwin":
123
+ return _fingerprint_macos()
124
+ else:
125
+ return _fingerprint_linux()
126
+
127
+
128
+ def _fingerprint_windows() -> str:
129
+ """Windows:使用 PowerShell Get-CimInstance 读取 CPU 序列号 + 硬盘序列号(替代已弃用的 wmic)。"""
130
+ parts = []
131
+
132
+ # CPU ProcessorId - 使用 PowerShell Get-CimInstance 替代 wmic
133
+ try:
134
+ result = subprocess.check_output(
135
+ ["powershell", "-Command", "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty ProcessorId"],
136
+ stderr=subprocess.DEVNULL,
137
+ timeout=5,
138
+ ).decode(errors="replace").strip()
139
+ if result:
140
+ parts.append(result)
141
+ except Exception:
142
+ pass
143
+
144
+ # 物理硬盘序列号 - 使用 PowerShell Get-CimInstance 替代 wmic
145
+ try:
146
+ result = subprocess.check_output(
147
+ ["powershell", "-Command", "Get-CimInstance -ClassName Win32_DiskDrive | Select-Object -ExpandProperty SerialNumber | Select-Object -First 1"],
148
+ stderr=subprocess.DEVNULL,
149
+ timeout=5,
150
+ ).decode(errors="replace").strip()
151
+ if result:
152
+ parts.append(result)
153
+ except Exception:
154
+ pass
155
+
156
+ if parts:
157
+ return "|".join(parts)
158
+
159
+ # 退化:hostname
160
+ return platform.node() + "|" + platform.machine()
161
+
162
+
163
+ def _fingerprint_macos() -> str:
164
+ """macOS:system_profiler 读取 Hardware UUID。"""
165
+ try:
166
+ result = subprocess.check_output(
167
+ ["system_profiler", "SPHardwareDataType"],
168
+ stderr=subprocess.DEVNULL,
169
+ timeout=5,
170
+ ).decode(errors="replace")
171
+ for line in result.splitlines():
172
+ # 精确匹配 "Hardware UUID",避免匹配到其他含 UUID 的行
173
+ if "Hardware UUID" in line:
174
+ return line.split(":")[-1].strip()
175
+ except Exception:
176
+ pass
177
+ return platform.node() + "|" + platform.machine()
178
+
179
+
180
+ def _fingerprint_linux() -> str:
181
+ """Linux:读取 /etc/machine-id。"""
182
+ for path in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
183
+ try:
184
+ machine_id = Path(path).read_text().strip()
185
+ if machine_id:
186
+ return machine_id
187
+ except Exception:
188
+ pass
189
+ return platform.node() + "|" + platform.machine()
190
+
191
+
192
+ # -------------------- 客户端 --------------------
193
+
194
+ class MyShareError(Exception):
195
+ """myshare 调用错误"""
196
+
197
+
198
+ def _build_retry_session() -> requests.Session:
199
+ """构建带重试策略的 requests.Session。
200
+
201
+ 重试条件:网络连接错误、502/503/504 状态码,最多重试 3 次,指数退避。
202
+
203
+ SSL 证书:
204
+ 生产环境 Nginx 通常配置 Let's Encrypt 证书(自动验证通过)。
205
+ 如果使用自签名证书,设置环境变量 MYSHARE_SSL_VERIFY=false 跳过验证。
206
+ """
207
+ session = requests.Session()
208
+ retry = Retry(
209
+ total=3,
210
+ backoff_factor=0.5,
211
+ status_forcelist=[502, 503, 504],
212
+ allowed_methods=["POST", "GET"],
213
+ )
214
+ adapter = HTTPAdapter(max_retries=retry)
215
+ session.mount("http://", adapter)
216
+ session.mount("https://", adapter)
217
+ # 通过环境变量控制 SSL 证书验证(默认开启)
218
+ session.verify = os.environ.get("MYSHARE_SSL_VERIFY", "true").lower() != "false"
219
+ return session
220
+
221
+
222
+ class MyShareClient:
223
+ """myshare 客户端
224
+
225
+ 参数:
226
+ auth_code: 由 myshare 服务端签发的用户访问 token
227
+ base_url: 网关地址(留空则使用内置默认值)
228
+ timeout: 请求超时时间(秒)
229
+ """
230
+
231
+ def __init__(
232
+ self,
233
+ auth_code: Optional[str] = None,
234
+ base_url: Optional[str] = None,
235
+ timeout: int = 30,
236
+ ) -> None:
237
+ self.auth_code = auth_code
238
+ # 优先级:显式传入 > 环境变量 > 内置混淆默认值
239
+ if base_url is not None:
240
+ self.base_url = base_url
241
+ else:
242
+ env_url = os.getenv("MYSHARE_BASE_URL")
243
+ self.base_url = env_url if env_url else _default_base_url()
244
+ self.timeout = timeout
245
+ self._session = _build_retry_session()
246
+ # 懒加载设备 ID(第一次调用时才采集,避免导入慢)
247
+ self._device_id: Optional[str] = None
248
+
249
+ def _get_device_id(self) -> str:
250
+ if self._device_id is None:
251
+ self._device_id = _get_device_id()
252
+ return self._device_id
253
+
254
+ # -------------------- 公共 API --------------------
255
+ def query(self, api_name: str, **params: Any) -> pd.DataFrame:
256
+ """通用查询接口,对应 tushare 的 pro.query(api_name, **params)"""
257
+ # 提前校验 auth_code,给出明确的错误提示
258
+ if not self.auth_code:
259
+ raise MyShareError(
260
+ "auth_code 未设置,请先调用 ts.set_token('your_code') 或在 pro_api() 中传入 auth_code 参数"
261
+ )
262
+
263
+ payload = {
264
+ "api_name": api_name,
265
+ "params": params or {},
266
+ "auth_code": self.auth_code,
267
+ "meta": {
268
+ "client": "python-sdk",
269
+ "device_id": self._get_device_id(),
270
+ },
271
+ }
272
+ url = self.base_url.rstrip("/") + "/api/v1/queue"
273
+ logger.debug("myshare request api=%s (async)", api_name)
274
+
275
+ try:
276
+ resp = self._session.post(url, json=payload, timeout=self.timeout)
277
+ except Exception as e:
278
+ logger.exception("请求 myshare 失败: %s", e)
279
+ raise MyShareError(f"request failed: {e}") from e
280
+
281
+ if resp.status_code != 200:
282
+ raise MyShareError(f"http status {resp.status_code}: {resp.text}")
283
+
284
+ try:
285
+ obj = resp.json()
286
+ except ValueError as e:
287
+ raise MyShareError(f"invalid json response: {e}") from e
288
+
289
+ code = obj.get("code", -1)
290
+ message = obj.get("message", "unknown error")
291
+ data = obj.get("data")
292
+
293
+ if code != 0:
294
+ raise MyShareError(f"error code {code}: {message}")
295
+
296
+ # 使用 is None 而非 not data,避免将空字典/空列表误判为"无数据"
297
+ if data is None:
298
+ return pd.DataFrame()
299
+
300
+ # 检查是否是异步任务响应(包含task_id)
301
+ if isinstance(data, dict) and "task_id" in data:
302
+ # 异步任务,需要轮询获取结果
303
+ task_id = data["task_id"]
304
+ return self._poll_async_result(task_id)
305
+ else:
306
+ # 同步响应或直接返回数据(兼容模式)
307
+ fields = data.get("fields") or []
308
+ items = data.get("items") or []
309
+ return pd.DataFrame(items, columns=fields)
310
+
311
+ def _poll_async_result(self, task_id: str, timeout: int = 60) -> pd.DataFrame:
312
+ """轮询异步任务结果
313
+
314
+ Args:
315
+ task_id: 异步任务ID
316
+ timeout: 轮询超时时间(秒)
317
+
318
+ Returns:
319
+ pandas DataFrame
320
+
321
+ Raises:
322
+ MyShareError: 任务失败或超时
323
+ """
324
+ import time
325
+
326
+ start_time = time.time()
327
+ poll_interval = 0.5 # 轮询间隔0.5秒
328
+
329
+ while time.time() - start_time < timeout:
330
+ # 查询任务状态
331
+ result_url = self.base_url.rstrip("/") + f"/api/v1/task/{task_id}"
332
+ try:
333
+ result_resp = self._session.get(result_url, timeout=self.timeout)
334
+ except Exception as e:
335
+ logger.exception("查询异步任务结果失败: %s", e)
336
+ raise MyShareError(f"query async result failed: {e}") from e
337
+
338
+ if result_resp.status_code != 200:
339
+ raise MyShareError(f"http status {result_resp.status_code}: {result_resp.text}")
340
+
341
+ try:
342
+ result_obj = result_resp.json()
343
+ except ValueError as e:
344
+ raise MyShareError(f"invalid json response: {e}") from e
345
+
346
+ result_code = result_obj.get("code", -1)
347
+
348
+ if result_code != 0:
349
+ raise MyShareError(f"error code {result_code}: {result_obj.get('message')}")
350
+
351
+ result_data = result_obj.get("data", {})
352
+ status = result_data.get("status")
353
+
354
+ if status == "done":
355
+ # 任务完成,返回数据
356
+ final_result = result_data.get("result", {})
357
+ fields = final_result.get("fields") or []
358
+ items = final_result.get("items") or []
359
+ logger.debug("异步任务完成: %s, 耗时: %.2f秒", task_id, time.time() - start_time)
360
+ return pd.DataFrame(items, columns=fields)
361
+ elif status == "failed":
362
+ # 任务失败
363
+ error = result_data.get("error", "unknown error")
364
+ logger.error("异步任务失败: %s, 错误: %s", task_id, error)
365
+ raise MyShareError(f"async task failed: {error}")
366
+ elif status == "pending":
367
+ # 任务还在处理中,继续等待
368
+ pass
369
+ else:
370
+ # 未知状态
371
+ logger.error("未知的异步任务状态: %s, task_id: %s", status, task_id)
372
+ raise MyShareError(f"unknown task status: {status}")
373
+
374
+ # 等待后继续轮询
375
+ time.sleep(poll_interval)
376
+
377
+ # 超时
378
+ logger.error("异步任务超时: %s, 超时时间: %d秒", task_id, timeout)
379
+ raise MyShareError(f"async task timeout after {timeout} seconds")
380
+
381
+ # -------------------- 动态 API 映射 --------------------
382
+ def __getattr__(self, name: str) -> Callable[..., pd.DataFrame]:
383
+ """将未知属性当作 api_name 动态映射
384
+
385
+ 例如:pro.daily(ts_code="000001.SZ") 等价于 query("daily", ts_code="000001.SZ")
386
+
387
+ 注意:__getattr__ 只在普通属性查找失败时触发,无需再做 hasattr 检查
388
+ (hasattr 检查会导致递归调用 __getattr__ 进而引发 RecursionError)。
389
+ """
390
+ if name.startswith("_"):
391
+ raise AttributeError(name)
392
+
393
+ def _wrapper(**kwargs: Any) -> pd.DataFrame:
394
+ return self.query(name, **kwargs)
395
+
396
+ _wrapper.__name__ = name
397
+ _wrapper.__doc__ = f"Dynamic API proxy for '{name}' via myshare."
398
+ return _wrapper
399
+
400
+
401
+ def pro_api(
402
+ auth_code: Optional[str] = None,
403
+ base_url: Optional[str] = None,
404
+ timeout: int = 30,
405
+ ) -> MyShareClient:
406
+ """创建 myshare 客户端实例(兼容 tushare.pro_api 接口风格)
407
+
408
+ auth_code 可省略,若已通过 set_token() 全局配置则自动读取。
409
+
410
+ 示例:
411
+ ts.set_token("msu_xxx")
412
+ pro = ts.pro_api()
413
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20240201")
414
+ """
415
+ final_auth_code = auth_code if auth_code is not None else _global_auth_code
416
+ final_base_url = base_url if base_url is not None else _global_base_url
417
+ return MyShareClient(auth_code=final_auth_code, base_url=final_base_url, timeout=timeout)
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: tsshare
3
+ Version: 1.0.0
4
+ Summary: tsshare — Tushare data proxy SDK, drop-in replacement for tushare pro_api
5
+ Author:
6
+ Author-email: tsshare team <tsshare@example.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/LinMason168/tsshare
9
+ Project-URL: Documentation, https://github.com/LinMason168/tsshare#readme
10
+ Project-URL: Repository, https://github.com/LinMason168/tsshare
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Office/Business :: Financial
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: requests>=2.28.0
26
+ Requires-Dist: pandas>=1.5.0
27
+ Requires-Dist: urllib3>=1.26.0
28
+ Dynamic: license-file
29
+ Dynamic: requires-python
30
+
31
+ # myshare SDK
32
+
33
+ A drop-in replacement for Tushare's `pro_api` — same API, no quota limits.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install myshare
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ import myshare as ts
45
+
46
+ # One-line setup: paste your auth_code
47
+ ts.set_token("your-auth-code")
48
+
49
+ # Use exactly like tushare pro_api
50
+ pro = ts.pro_api()
51
+ df = pro.daily(ts_code="000001.SZ", start_date="20240101", end_date="20240201")
52
+ print(df)
53
+ ```
54
+
55
+ ## Supported APIs
56
+
57
+ All Tushare APIs are supported: `daily`, `stock_basic`, `trade_cal`, `income`, `balancesheet`, `moneyflow`, and more.
58
+
59
+ ## Platform Support
60
+
61
+ - Windows (x86_64)
62
+ - macOS (Apple Silicon / Intel)
63
+ - Linux (x86_64 / aarch64)
64
+
65
+ Python 3.8 or later required.
66
+
67
+ ## License
68
+
69
+ MIT
@@ -0,0 +1,7 @@
1
+ tsshare/__init__.py,sha256=04Rs88HupzmrOHkE9FypDtRgGF4nF2BtCCrhXSfzZoU,622
2
+ tsshare/client.py,sha256=Ah1fEdJjk2uZfAxn2VtiK8b1I2O4jY9B_qFVmlHFYzQ,15286
3
+ tsshare-1.0.0.dist-info/licenses/LICENSE,sha256=mfZBsenkJrxFQ7Gidx0tef34i03o8ectL7JUSypwsYk,1064
4
+ tsshare-1.0.0.dist-info/METADATA,sha256=fFMYGPo4-Pmn7X8B23yaZ5Lk89qKPO7EU1rg2QZmlzA,1979
5
+ tsshare-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ tsshare-1.0.0.dist-info/top_level.txt,sha256=y3Fcx9aqp9-L9yGQ2v8kEFexuqRRIfOKwHKDQeAMwn4,8
7
+ tsshare-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 myshare
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ tsshare