datasinking 0.2.6__tar.gz → 0.2.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasinking
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Python client for DataSinking — full-text Asian financial reports (China, Korea, Japan, Taiwan) as Markdown.
5
5
  Author: DataSinking
6
6
  License: MIT
@@ -18,6 +18,7 @@ License-File: LICENSE
18
18
  Provides-Extra: mcp
19
19
  Requires-Dist: mcp>=1.0.0; extra == "mcp"
20
20
  Requires-Dist: requests>=2.28; extra == "mcp"
21
+ Requires-Dist: pydantic>=2.0; extra == "mcp"
21
22
  Dynamic: license-file
22
23
 
23
24
  # DataSinking
@@ -111,7 +112,7 @@ coverage, list a company's reports, and extract a figure with correct units.
111
112
 
112
113
  Every example pulls from the live API and runs as-is.
113
114
 
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
+ > `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents). Quotas count **documents, not requests**, over a rolling 7-day window: a free key gets 3 req/s and 8,191 documents per 7 days, inside a pool of 524,287 per 7 days shared by all free users and website visitors. A whole exchange will therefore take well over a week on a free key — a **paid (yearly)** key (31 req/s, 524,287 documents per 7 days) is strongly recommended.
115
116
 
116
117
  ## Research (`research/`)
117
118
 
@@ -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). 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.
92
+ > `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents). Quotas count **documents, not requests**, over a rolling 7-day window: a free key gets 3 req/s and 8,191 documents per 7 days, inside a pool of 524,287 per 7 days shared by all free users and website visitors. A whole exchange will therefore take well over a week on a free key — a **paid (yearly)** key (31 req/s, 524,287 documents per 7 days) is strongly recommended.
93
93
 
94
94
  ## Research (`research/`)
95
95
 
@@ -11,7 +11,7 @@ Usage:
11
11
  try:
12
12
  ds.get_stock_reports("600519.SS", limit=-1)
13
13
  except QuotaExceeded as e:
14
- print(e.code, e) # e.g. quota_month / free_quota_global
14
+ print(e.code, e) # e.g. quota_7d / free_quota_key / free_quota_global
15
15
  """
16
16
  from ._version import __version__
17
17
  from .client import DataSinking, QuotaExceeded
@@ -1,11 +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.6"
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.8"
@@ -24,14 +24,13 @@ class QuotaExceeded(RuntimeError):
24
24
  """额度用尽(HTTP 429 + 服务端返回的 `code`)。
25
25
 
26
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 天额度用尽(所有免费用户合计)
27
+ 额度类要等**7 天窗口滚动**才有用 —— 重试没有意义,所以直接抛出来,不再重试。
28
+
29
+ 常见 code(2026-09-21 起配额统一成「每 7 天 rolling」,日/月两层已取消):
30
+ quota_7d 年费 key 7 天额度用尽(每 7 天 524,287 篇)
31
+ free_quota_key 免费 key 7 天额度用尽(每 7 天 8,191 篇)
32
+ free_quota_global 免费共享池的 7 天额度用尽
33
+ (所有免费 key + 网页访客合计每 7 天 524,287 篇)
35
34
  """
36
35
 
37
36
  def __init__(self, code=None, detail=None):
@@ -81,8 +80,8 @@ class DataSinking:
81
80
  raise # 401/403/404/400… 直接抛给上层
82
81
  code, detail = _error_info(e)
83
82
  if code:
84
- # 额度类 429:要等日/月窗口滚动,重试没有意义 —— 立刻抛,并带上服务端的原因。
85
- # (以前这里不分青红皂白 sleep(2) 重试,月度额度打满时会空转,
83
+ # 额度类 429:要等 7 天窗口滚动,重试没有意义 —— 立刻抛,并带上服务端的原因。
84
+ # (以前这里不分青红皂白 sleep(2) 重试,额度打满时会空转,
86
85
  # 最后还抛出一句 "Request failed after retries: None" —— last 变量
87
86
  # 只在网络异常分支被赋值,429 分支根本不赋值。)
88
87
  raise QuotaExceeded(code, detail)
@@ -0,0 +1,165 @@
1
+ # -*- coding: utf-8 -*-
2
+ """DataSinking MCP server (Model Context Protocol).
3
+
4
+ Expose the DataSinking API — full-text financial reports across Asia
5
+ (China, Korea, Japan) as clean Markdown — to AI agents (Claude, Cursor,
6
+ Codex, DeepSeek, Windsurf, …).
7
+
8
+ Install the MCP extra::
9
+
10
+ pip install "datasinking[mcp]"
11
+
12
+ Then run::
13
+
14
+ datasinking-mcp
15
+
16
+ or add to any MCP client with ``command: datasinking-mcp`` (stdio). Requires
17
+ the environment variable ``DATASINK_API_KEY`` (get a free key at
18
+ https://datasink.ing).
19
+ """
20
+
21
+ import os
22
+ from typing import Annotated, Optional
23
+
24
+ import requests
25
+ from pydantic import Field
26
+
27
+ from ._version import __version__ # 版本号唯一来源(原来是硬编码,漂到了 0.2.3)
28
+
29
+ # mcp v1 uses FastMCP; v2 renamed it to MCPServer. Support both.
30
+ try:
31
+ from mcp.server.fastmcp import FastMCP # mcp v1
32
+ except ImportError: # pragma: no cover
33
+ from mcp.server.mcpserver import MCPServer as FastMCP # mcp v2
34
+
35
+ # ⚠️ 参数描述必须写成 `Annotated[T, Field(description=...)]`,**不能只靠 docstring 的 Args 段**。
36
+ # mcp v2(MCPServer)不再解析 docstring 的 Args —— 实测把整段 Args 当散文塞进工具描述,
37
+ # 参数级 description 全是空的(2026-09-22 用真实 stdio 握手验证)。v1 两种都认,所以这样写两边通用。
38
+
39
+
40
+ BASE_URL = "https://api.datasink.ing"
41
+ API_KEY = os.environ.get("DATASINK_API_KEY", "")
42
+
43
+ mcp = FastMCP(
44
+ "DataSinking",
45
+ title="DataSinking — Full-text Asian Financial Reports",
46
+ description="Full-text Asian financial reports (China, Korea, Japan, Taiwan) as clean Markdown via API, with chapter-level access for RAG and AI agents.",
47
+ version=__version__,
48
+ instructions=(
49
+ "DataSinking serves full-text financial reports (annual / semi-annual / quarterly) "
50
+ "from China, Korea, Japan and Taiwan as clean Markdown, ready for LLM reading and RAG. "
51
+ "Use FMP-style symbols: 600519.SS (Kweichow Moutai), 005930.KS (Samsung Electronics), "
52
+ "7203.T (Toyota), 2330.TW (TSMC). To save tokens, prefer get_section to pull one chapter "
53
+ "(e.g. MD&A) instead of get_report for the whole document."
54
+ ),
55
+ )
56
+
57
+
58
+ def _get(path: str, params: Optional[dict] = None) -> dict:
59
+ """Call the DataSinking API, carrying the API key automatically."""
60
+ if not API_KEY:
61
+ raise RuntimeError(
62
+ "Missing DATASINK_API_KEY environment variable (get a free key at https://datasink.ing)"
63
+ )
64
+ p = dict(params or {})
65
+ p["apikey"] = API_KEY
66
+ r = requests.get(f"{BASE_URL}{path}", params=p, timeout=90)
67
+ r.raise_for_status()
68
+ return r.json()
69
+
70
+
71
+ @mcp.tool()
72
+ def list_exchanges() -> list:
73
+ """List the exchanges DataSinking covers and their report counts.
74
+
75
+ Returns exchange codes (sse / szse / bj / ksc / koe / knx / jpx / twse / tpex) with
76
+ the number of reports available per exchange. Call this first to discover coverage.
77
+ Sources: A-shares = cninfo.com.cn, Korea = DART, Japan = EDINET, Taiwan = MOPS.
78
+ """
79
+ return _get("/exchanges").get("exchanges", [])
80
+
81
+
82
+ @mcp.tool()
83
+ def list_stocks(
84
+ exchange: Annotated[
85
+ str, Field(description="Exchange code: sse / szse / bj / ksc / koe / knx / jpx / twse / tpex")
86
+ ],
87
+ limit: Annotated[
88
+ int, Field(description="Return only the first N companies (default 20) to keep the response short.")
89
+ ] = 20,
90
+ ) -> dict:
91
+ """List stocks on an exchange, including the report count per company."""
92
+ data = _get("/stocks", {"exchange": exchange})
93
+ return {"exchange": exchange, "total": data.get("total", 0), "items": data.get("items", [])[:limit]}
94
+
95
+
96
+ @mcp.tool()
97
+ def list_reports(
98
+ symbol: Annotated[
99
+ str, Field(description="FMP-style symbol, e.g. 600519.SS / 005930.KS / 7203.T / 2330.TW")
100
+ ],
101
+ doc_type: Annotated[
102
+ str, Field(description="annual / semiannual / q1 / q3")
103
+ ] = "annual",
104
+ size: Annotated[int, Field(description="Number of reports to return (default 10).")] = 10,
105
+ ) -> dict:
106
+ """List a company's reports — metadata only (id, title, period), no body text.
107
+
108
+ Each item carries a ``source`` field naming the official disclosure platform;
109
+ keep that attribution when you cite it.
110
+ """
111
+ return _get("/documents", {"symbol": symbol, "doc_type": doc_type, "size": size})
112
+
113
+
114
+ @mcp.tool()
115
+ def get_report(
116
+ document_id: Annotated[int, Field(description="Report id, from list_reports items[].id")],
117
+ ) -> dict:
118
+ """Fetch a single report's full text (metadata + Markdown body).
119
+
120
+ The ``source`` field names the official disclosure platform; keep that attribution
121
+ when you cite it. Expensive in tokens — prefer get_section when you only need one chapter.
122
+ """
123
+ return _get(f"/documents/{document_id}")
124
+
125
+
126
+ @mcp.tool()
127
+ def list_sections(
128
+ document_id: Annotated[int, Field(description="Report id, from list_reports items[].id")],
129
+ ) -> dict:
130
+ """List every section heading in a report (feed the headings to get_section).
131
+
132
+ Call this before get_section to see the exact headings — the headings are in the
133
+ report's own language.
134
+ """
135
+ return _get(f"/documents/{document_id}/sections")
136
+
137
+
138
+ @mcp.tool()
139
+ def get_section(
140
+ document_id: Annotated[int, Field(description="Report id, from list_reports items[].id")],
141
+ section: Annotated[
142
+ str,
143
+ Field(
144
+ description=(
145
+ "Heading keyword, matched as a substring against the report's OWN headings, "
146
+ "so pass it in the report's language. A-share reports have Chinese headings "
147
+ "(e.g. 第三节管理层讨论与分析) — use 管理层讨论与分析 / 财务报告 there. "
148
+ "For English-language filings, \"MD&A\" / \"financial statements\" / \"notes\" work. "
149
+ "If nothing matches, the API returns 404 with the real headings — retry with one "
150
+ "of those, or call list_sections first."
151
+ )
152
+ ),
153
+ ],
154
+ ) -> dict:
155
+ """Fetch only one section of a report by keyword — cheaper than get_report for RAG."""
156
+ return _get(f"/documents/{document_id}", {"section": section})
157
+
158
+
159
+ def main() -> None:
160
+ """Entry point for the ``datasinking-mcp`` console script."""
161
+ mcp.run()
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasinking
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Python client for DataSinking — full-text Asian financial reports (China, Korea, Japan, Taiwan) as Markdown.
5
5
  Author: DataSinking
6
6
  License: MIT
@@ -18,6 +18,7 @@ License-File: LICENSE
18
18
  Provides-Extra: mcp
19
19
  Requires-Dist: mcp>=1.0.0; extra == "mcp"
20
20
  Requires-Dist: requests>=2.28; extra == "mcp"
21
+ Requires-Dist: pydantic>=2.0; extra == "mcp"
21
22
  Dynamic: license-file
22
23
 
23
24
  # DataSinking
@@ -111,7 +112,7 @@ coverage, list a company's reports, and extract a figure with correct units.
111
112
 
112
113
  Every example pulls from the live API and runs as-is.
113
114
 
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
+ > `03_download_exchange.py` fetches every report on an exchange (e.g. all of Shenzhen — 150k+ documents). Quotas count **documents, not requests**, over a rolling 7-day window: a free key gets 3 req/s and 8,191 documents per 7 days, inside a pool of 524,287 per 7 days shared by all free users and website visitors. A whole exchange will therefore take well over a week on a free key — a **paid (yearly)** key (31 req/s, 524,287 documents per 7 days) is strongly recommended.
115
116
 
116
117
  ## Research (`research/`)
117
118
 
@@ -2,3 +2,4 @@
2
2
  [mcp]
3
3
  mcp>=1.0.0
4
4
  requests>=2.28
5
+ pydantic>=2.0
@@ -22,7 +22,7 @@ classifiers = [
22
22
  dependencies = []
23
23
 
24
24
  [project.optional-dependencies]
25
- mcp = ["mcp>=1.0.0", "requests>=2.28"]
25
+ mcp = ["mcp>=1.0.0", "requests>=2.28", "pydantic>=2.0"]
26
26
 
27
27
  [project.scripts]
28
28
  datasinking-mcp = "datasinking.mcp_server:main"
@@ -1,134 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """DataSinking MCP server (Model Context Protocol).
3
-
4
- Expose the DataSinking API — full-text financial reports across Asia
5
- (China, Korea, Japan) as clean Markdown — to AI agents (Claude, Cursor,
6
- Codex, DeepSeek, Windsurf, …).
7
-
8
- Install the MCP extra::
9
-
10
- pip install "datasinking[mcp]"
11
-
12
- Then run::
13
-
14
- datasinking-mcp
15
-
16
- or add to any MCP client with ``command: datasinking-mcp`` (stdio). Requires
17
- the environment variable ``DATASINK_API_KEY`` (get a free key at
18
- https://datasink.ing).
19
- """
20
-
21
- import os
22
- from typing import Optional
23
-
24
- import requests
25
-
26
- # mcp v1 uses FastMCP; v2 renamed it to MCPServer. Support both.
27
- try:
28
- from mcp.server.fastmcp import FastMCP # mcp v1
29
- except ImportError: # pragma: no cover
30
- from mcp.server.mcpserver import MCPServer as FastMCP # mcp v2
31
-
32
- BASE_URL = "https://api.datasink.ing"
33
- API_KEY = os.environ.get("DATASINK_API_KEY", "")
34
-
35
- mcp = FastMCP(
36
- "DataSinking",
37
- title="DataSinking — Full-text Asian Financial Reports",
38
- description="Full-text Asian financial reports (China, Korea, Japan) as clean Markdown via API, with chapter-level access for RAG and AI agents.",
39
- version="0.2.3",
40
- instructions=(
41
- "DataSinking serves full-text financial reports (annual / semi-annual / quarterly) "
42
- "from China, Korea and Japan as clean Markdown, ready for LLM reading and RAG. "
43
- "Use FMP-style symbols: 600519.SS (Kweichow Moutai), 005930.KS (Samsung Electronics), "
44
- "7203.T (Toyota). To save tokens, prefer get_section to pull one chapter (e.g. MD&A) "
45
- "instead of get_report for the whole document."
46
- ),
47
- )
48
-
49
-
50
- def _get(path: str, params: Optional[dict] = None) -> dict:
51
- """Call the DataSinking API, carrying the API key automatically."""
52
- if not API_KEY:
53
- raise RuntimeError(
54
- "Missing DATASINK_API_KEY environment variable (get a free key at https://datasink.ing)"
55
- )
56
- p = dict(params or {})
57
- p["apikey"] = API_KEY
58
- r = requests.get(f"{BASE_URL}{path}", params=p, timeout=90)
59
- r.raise_for_status()
60
- return r.json()
61
-
62
-
63
- @mcp.tool()
64
- def list_exchanges() -> list:
65
- """List the exchanges DataSinking covers and their report counts.
66
-
67
- Returns exchange codes (sse / szse / bj / ksc / koe / knx / jpx) with the number
68
- of reports available per exchange. Call this first to discover coverage.
69
- """
70
- return _get("/exchanges").get("exchanges", [])
71
-
72
-
73
- @mcp.tool()
74
- def list_stocks(exchange: str, limit: int = 20) -> dict:
75
- """List stocks on an exchange, including the report count per company.
76
-
77
- Args:
78
- exchange: Exchange code, e.g. sse / szse / bj / ksc / koe / knx / jpx
79
- limit: Return the first N companies (default 20) to keep responses short.
80
- """
81
- data = _get("/stocks", {"exchange": exchange})
82
- return {"exchange": exchange, "total": data.get("total", 0), "items": data.get("items", [])[:limit]}
83
-
84
-
85
- @mcp.tool()
86
- def list_reports(symbol: str, doc_type: str = "annual", size: int = 10) -> dict:
87
- """List a company's reports — metadata only (id, title, period), no body text.
88
-
89
- Args:
90
- symbol: FMP-style symbol, e.g. 600519.SS / 005930.KS / 7203.T
91
- doc_type: annual / semiannual / q1 / q3
92
- size: Number of reports to return (default 10).
93
- """
94
- return _get("/documents", {"symbol": symbol, "doc_type": doc_type, "size": size})
95
-
96
-
97
- @mcp.tool()
98
- def get_report(document_id: int) -> dict:
99
- """Fetch a single report's full text (metadata + Markdown body).
100
-
101
- Args:
102
- document_id: Report id, from list_reports items[].id
103
- """
104
- return _get(f"/documents/{document_id}")
105
-
106
-
107
- @mcp.tool()
108
- def list_sections(document_id: int) -> dict:
109
- """List every section heading in a report (feed the headings to get_section).
110
-
111
- Args:
112
- document_id: Report id
113
- """
114
- return _get(f"/documents/{document_id}/sections")
115
-
116
-
117
- @mcp.tool()
118
- def get_section(document_id: int, section: str) -> dict:
119
- """Fetch only one section of a report by keyword — cheaper than get_report for RAG.
120
-
121
- Args:
122
- document_id: Report id
123
- section: Heading keyword, e.g. "management discussion" / "MD&A" / "financial statements" / "notes"
124
- """
125
- return _get(f"/documents/{document_id}", {"section": section})
126
-
127
-
128
- def main() -> None:
129
- """Entry point for the ``datasinking-mcp`` console script."""
130
- mcp.run()
131
-
132
-
133
- if __name__ == "__main__":
134
- main()
File without changes
File without changes