datasinking 0.2.4__tar.gz → 0.2.5__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.
- {datasinking-0.2.4 → datasinking-0.2.5}/PKG-INFO +2 -2
- {datasinking-0.2.4 → datasinking-0.2.5}/README.md +1 -1
- datasinking-0.2.5/datasinking/__init__.py +19 -0
- datasinking-0.2.5/datasinking/_version.py +11 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking/client.py +85 -23
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking.egg-info/PKG-INFO +2 -2
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking.egg-info/SOURCES.txt +1 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/pyproject.toml +6 -1
- datasinking-0.2.4/datasinking/__init__.py +0 -13
- {datasinking-0.2.4 → datasinking-0.2.5}/LICENSE +0 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking/mcp_server.py +0 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking.egg-info/dependency_links.txt +0 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking.egg-info/entry_points.txt +0 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking.egg-info/requires.txt +0 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/datasinking.egg-info/top_level.txt +0 -0
- {datasinking-0.2.4 → datasinking-0.2.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datasinking
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Python client for DataSinking — full-text Asian financial reports (China, Korea, Japan) as Markdown.
|
|
5
5
|
Author: DataSinking
|
|
6
6
|
License: MIT
|
|
@@ -111,7 +111,7 @@ coverage, list a company's reports, and extract a figure with correct units.
|
|
|
111
111
|
|
|
112
112
|
Every example pulls from the live API and runs as-is.
|
|
113
113
|
|
|
114
|
-
> `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents).
|
|
114
|
+
> `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents). Quotas count **documents, not requests**, and apply over a rolling 31-day window as well as per day: a free key gets 3 req/s and 8,191 documents/day, inside a pool of 131,071/day and 524,287 per 31 days shared by all free users. A whole exchange will therefore take more than a day on a free key — a **paid (yearly)** key (31 req/s, 131,071 documents/day, 524,287 per 31 days) is strongly recommended.
|
|
115
115
|
|
|
116
116
|
## Research (`research/`)
|
|
117
117
|
|
|
@@ -89,7 +89,7 @@ coverage, list a company's reports, and extract a figure with correct units.
|
|
|
89
89
|
|
|
90
90
|
Every example pulls from the live API and runs as-is.
|
|
91
91
|
|
|
92
|
-
> `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents).
|
|
92
|
+
> `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents). Quotas count **documents, not requests**, and apply over a rolling 31-day window as well as per day: a free key gets 3 req/s and 8,191 documents/day, inside a pool of 131,071/day and 524,287 per 31 days shared by all free users. A whole exchange will therefore take more than a day on a free key — a **paid (yearly)** key (31 req/s, 131,071 documents/day, 524,287 per 31 days) is strongly recommended.
|
|
93
93
|
|
|
94
94
|
## Research (`research/`)
|
|
95
95
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DataSinking Python client.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from datasinking import DataSinking
|
|
6
|
+
ds = DataSinking("YOUR_API_KEY")
|
|
7
|
+
ds.list_exchanges()
|
|
8
|
+
|
|
9
|
+
额度用尽时抛 `QuotaExceeded`(区别于网络/限流错误):
|
|
10
|
+
from datasinking import DataSinking, QuotaExceeded
|
|
11
|
+
try:
|
|
12
|
+
ds.get_stock_reports("600519.SS", limit=-1)
|
|
13
|
+
except QuotaExceeded as e:
|
|
14
|
+
print(e.code, e) # e.g. quota_month / free_quota_global
|
|
15
|
+
"""
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
from .client import DataSinking, QuotaExceeded
|
|
18
|
+
|
|
19
|
+
__all__ = ["DataSinking", "QuotaExceeded", "__version__"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""版本号的**唯一来源**。
|
|
3
|
+
|
|
4
|
+
pyproject.toml 用 `dynamic = ["version"]` 从这里读,`client.py` 的 User-Agent 也读它。
|
|
5
|
+
|
|
6
|
+
为什么单独抽一个文件:之前 pyproject.toml 和 `__init__.py` 各写各的版本号,
|
|
7
|
+
结果 `__version__` 长期停在 0.1.0 没跟上(发 0.2.4 时才发现,User-Agent 里也一直
|
|
8
|
+
写着旧版本)。现在只有这一处需要改。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.5"
|
|
@@ -17,11 +17,48 @@ import urllib.request
|
|
|
17
17
|
import urllib.error
|
|
18
18
|
import urllib.parse
|
|
19
19
|
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QuotaExceeded(RuntimeError):
|
|
24
|
+
"""额度用尽(HTTP 429 + 服务端返回的 `code`)。
|
|
25
|
+
|
|
26
|
+
和「限流」不是一回事:限流等几秒就好(客户端自己会重试),
|
|
27
|
+
额度类要等**日/月窗口滚动**才有用 —— 重试没有意义,所以直接抛出来,不再重试。
|
|
28
|
+
|
|
29
|
+
常见 code:
|
|
30
|
+
quota_day 年费 key 当日额度用尽(UTC 次日 00:00 恢复)
|
|
31
|
+
quota_month 年费 key 最近 31 天额度用尽
|
|
32
|
+
free_quota_key 免费 key 当日额度用尽
|
|
33
|
+
free_quota_global 免费共享池 当日额度用尽(所有免费用户合计)
|
|
34
|
+
free_quota_month 免费共享池 最近 31 天额度用尽(所有免费用户合计)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, code=None, detail=None):
|
|
38
|
+
super().__init__(detail or code or "Quota exceeded")
|
|
39
|
+
self.code = code
|
|
40
|
+
self.detail = detail
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _error_info(e):
|
|
44
|
+
"""从错误响应体里取 (code, detail)。
|
|
45
|
+
|
|
46
|
+
服务端把 `code` 留给**非瞬时**的错误:限流(等几秒就好)不带 code,
|
|
47
|
+
额度类(要等窗口滚动)才带。所以「有没有 code」就是「该不该重试」的判据。
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
body = json.loads(e.read().decode("utf-8"))
|
|
51
|
+
return body.get("code"), body.get("detail")
|
|
52
|
+
except Exception:
|
|
53
|
+
return None, None
|
|
54
|
+
|
|
20
55
|
|
|
21
56
|
class DataSinking:
|
|
22
57
|
def __init__(self, api_key, base_url="https://api.datasink.ing"):
|
|
23
58
|
self.base_url = base_url.rstrip("/")
|
|
24
59
|
self.api_key = api_key
|
|
60
|
+
# batch 探测出来的可用块大小(免费档 3 / 年费 31)。首次调用后缓存,之后不再试错。
|
|
61
|
+
self._batch_size = None
|
|
25
62
|
|
|
26
63
|
# ---- 内部: HTTP ----
|
|
27
64
|
def _request(self, method, path, params=None, body=None, retries=5):
|
|
@@ -29,7 +66,7 @@ class DataSinking:
|
|
|
29
66
|
p["apikey"] = self.api_key
|
|
30
67
|
url = f"{self.base_url}{path}?{urllib.parse.urlencode(p)}"
|
|
31
68
|
data = None
|
|
32
|
-
headers = {"User-Agent": "Mozilla/5.0 (compatible; DataSinking/
|
|
69
|
+
headers = {"User-Agent": f"Mozilla/5.0 (compatible; DataSinking/{__version__})"}
|
|
33
70
|
if body is not None:
|
|
34
71
|
data = json.dumps(body).encode("utf-8")
|
|
35
72
|
headers["Content-Type"] = "application/json"
|
|
@@ -40,14 +77,22 @@ class DataSinking:
|
|
|
40
77
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
41
78
|
return json.loads(resp.read().decode("utf-8"))
|
|
42
79
|
except urllib.error.HTTPError as e:
|
|
43
|
-
if e.code
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
80
|
+
if e.code != 429:
|
|
81
|
+
raise # 401/403/404/400… 直接抛给上层
|
|
82
|
+
code, detail = _error_info(e)
|
|
83
|
+
if code:
|
|
84
|
+
# 额度类 429:要等日/月窗口滚动,重试没有意义 —— 立刻抛,并带上服务端的原因。
|
|
85
|
+
# (以前这里不分青红皂白 sleep(2) 重试,月度额度打满时会空转,
|
|
86
|
+
# 最后还抛出一句 "Request failed after retries: None" —— last 变量
|
|
87
|
+
# 只在网络异常分支被赋值,429 分支根本不赋值。)
|
|
88
|
+
raise QuotaExceeded(code, detail)
|
|
89
|
+
# 纯限流:等一下再试
|
|
90
|
+
last = RuntimeError(detail or "Rate limit exceeded (HTTP 429)")
|
|
91
|
+
time.sleep(2)
|
|
47
92
|
except (urllib.error.URLError, TimeoutError, OSError) as e:
|
|
48
93
|
time.sleep(1 + i)
|
|
49
94
|
last = e
|
|
50
|
-
raise RuntimeError(f"Request failed after retries: {last}")
|
|
95
|
+
raise RuntimeError(f"Request failed after {retries} retries: {last}")
|
|
51
96
|
|
|
52
97
|
def _get(self, path, params=None):
|
|
53
98
|
return self._request("GET", path, params)
|
|
@@ -67,24 +112,41 @@ class DataSinking:
|
|
|
67
112
|
page += 1
|
|
68
113
|
return items
|
|
69
114
|
|
|
70
|
-
def _batch_content(self, ids, batch_size=
|
|
71
|
-
"""
|
|
115
|
+
def _batch_content(self, ids, batch_size=None):
|
|
116
|
+
"""批量拉全文。
|
|
117
|
+
|
|
118
|
+
服务端**按档位**限制 batch 单次篇数(免费 3 / 年费 31),而客户端事先不知道自己
|
|
119
|
+
是哪档,所以先按 31 发;被拒(400 / 403)就把块缩小重试,最终退化成逐篇拉取。
|
|
120
|
+
31 → 10 → 3,免费档试错两次后稳定在 3;**探测出的可用大小会缓存到实例上**,
|
|
121
|
+
同一个 client 之后的调用不再重复试错。
|
|
122
|
+
|
|
123
|
+
旧版这里只认 403,而免费档超限返回的是 **400** —— 会直接 raise 把整个任务打断。
|
|
124
|
+
"""
|
|
125
|
+
if batch_size is None:
|
|
126
|
+
batch_size = self._batch_size or 31
|
|
72
127
|
items = []
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
128
|
+
i = 0
|
|
129
|
+
while i < len(ids):
|
|
130
|
+
n = min(batch_size, len(ids) - i)
|
|
131
|
+
chunk = ids[i : i + n]
|
|
132
|
+
|
|
133
|
+
if n == 1: # 块已经缩到 1 篇,batch 没意义,直接走单篇接口
|
|
134
|
+
items.append(self._get(f"/documents/{chunk[0]}"))
|
|
135
|
+
i += 1
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
items.extend(self._post("/documents/batch", {"doc_ids": chunk})["items"])
|
|
140
|
+
self._batch_size = n # 记住这个大小,下次直接用
|
|
141
|
+
i += n
|
|
142
|
+
except QuotaExceeded:
|
|
143
|
+
raise # 额度用尽,缩块也没用
|
|
144
|
+
except urllib.error.HTTPError as e:
|
|
145
|
+
if e.code not in (400, 403):
|
|
146
|
+
raise
|
|
147
|
+
# 被拒 → 缩块重试。大于 6 就除以 3(收敛快),小了就减一(保证能到 1、
|
|
148
|
+
# 且不会跳过服务端真实允许的大小,比如 12 → 4 → 3 而不是 12 → 4 → 1)。
|
|
149
|
+
batch_size = n // 3 if n > 6 else n - 1
|
|
88
150
|
return items
|
|
89
151
|
|
|
90
152
|
# ---- 5 个核心函数 ----
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datasinking
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Python client for DataSinking — full-text Asian financial reports (China, Korea, Japan) as Markdown.
|
|
5
5
|
Author: DataSinking
|
|
6
6
|
License: MIT
|
|
@@ -111,7 +111,7 @@ coverage, list a company's reports, and extract a figure with correct units.
|
|
|
111
111
|
|
|
112
112
|
Every example pulls from the live API and runs as-is.
|
|
113
113
|
|
|
114
|
-
> `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents).
|
|
114
|
+
> `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents). Quotas count **documents, not requests**, and apply over a rolling 31-day window as well as per day: a free key gets 3 req/s and 8,191 documents/day, inside a pool of 131,071/day and 524,287 per 31 days shared by all free users. A whole exchange will therefore take more than a day on a free key — a **paid (yearly)** key (31 req/s, 131,071 documents/day, 524,287 per 31 days) is strongly recommended.
|
|
115
115
|
|
|
116
116
|
## Research (`research/`)
|
|
117
117
|
|
|
@@ -4,7 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datasinking"
|
|
7
|
-
|
|
7
|
+
# 版本号唯一来源 = datasinking/_version.py(client.py 的 User-Agent 也读它)。
|
|
8
|
+
# 以前这里和包内 __version__ 各写各的,结果漂了半年(发 0.2.4 时才发现 __init__ 还停在 0.1.0)。
|
|
9
|
+
dynamic = ["version"]
|
|
8
10
|
description = "Python client for DataSinking — full-text Asian financial reports (China, Korea, Japan) as Markdown."
|
|
9
11
|
readme = "README.md"
|
|
10
12
|
requires-python = ">=3.8"
|
|
@@ -32,3 +34,6 @@ Repository = "https://github.com/heubme2020/datasinking"
|
|
|
32
34
|
|
|
33
35
|
[tool.setuptools]
|
|
34
36
|
packages = ["datasinking"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.dynamic]
|
|
39
|
+
version = { attr = "datasinking._version.__version__" }
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""DataSinking Python client.
|
|
3
|
-
|
|
4
|
-
Usage:
|
|
5
|
-
from datasinking import DataSinking
|
|
6
|
-
ds = DataSinking("YOUR_API_KEY")
|
|
7
|
-
ds.list_exchanges()
|
|
8
|
-
"""
|
|
9
|
-
from .client import DataSinking
|
|
10
|
-
|
|
11
|
-
# 与 pyproject.toml 的 version 保持一致 —— 这里曾长期停在 0.1.0 没跟着走
|
|
12
|
-
__version__ = "0.2.4"
|
|
13
|
-
__all__ = ["DataSinking"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|