datasinking 0.2.7__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.7
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
@@ -8,4 +8,4 @@ pyproject.toml 用 `dynamic = ["version"]` 从这里读,`client.py` 的 User-A
8
8
  写着旧版本)。现在只有这一处需要改。
9
9
  """
10
10
 
11
- __version__ = "0.2.7"
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)
@@ -19,9 +19,10 @@ https://datasink.ing).
19
19
  """
20
20
 
21
21
  import os
22
- from typing import Optional
22
+ from typing import Annotated, Optional
23
23
 
24
24
  import requests
25
+ from pydantic import Field
25
26
 
26
27
  from ._version import __version__ # 版本号唯一来源(原来是硬编码,漂到了 0.2.3)
27
28
 
@@ -31,6 +32,11 @@ try:
31
32
  except ImportError: # pragma: no cover
32
33
  from mcp.server.mcpserver import MCPServer as FastMCP # mcp v2
33
34
 
35
+ # ⚠️ 参数描述必须写成 `Annotated[T, Field(description=...)]`,**不能只靠 docstring 的 Args 段**。
36
+ # mcp v2(MCPServer)不再解析 docstring 的 Args —— 实测把整段 Args 当散文塞进工具描述,
37
+ # 参数级 description 全是空的(2026-09-22 用真实 stdio 握手验证)。v1 两种都认,所以这样写两边通用。
38
+
39
+
34
40
  BASE_URL = "https://api.datasink.ing"
35
41
  API_KEY = os.environ.get("DATASINK_API_KEY", "")
36
42
 
@@ -66,64 +72,87 @@ def _get(path: str, params: Optional[dict] = None) -> dict:
66
72
  def list_exchanges() -> list:
67
73
  """List the exchanges DataSinking covers and their report counts.
68
74
 
69
- Returns exchange codes (sse / szse / bj / ksc / koe / knx / jpx) with the number
70
- of reports available per exchange. Call this first to discover coverage.
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.
71
78
  """
72
79
  return _get("/exchanges").get("exchanges", [])
73
80
 
74
81
 
75
82
  @mcp.tool()
76
- def list_stocks(exchange: str, limit: int = 20) -> dict:
77
- """List stocks on an exchange, including the report count per company.
78
-
79
- Args:
80
- exchange: Exchange code, e.g. sse / szse / bj / ksc / koe / knx / jpx
81
- limit: Return the first N companies (default 20) to keep responses short.
82
- """
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."""
83
92
  data = _get("/stocks", {"exchange": exchange})
84
93
  return {"exchange": exchange, "total": data.get("total", 0), "items": data.get("items", [])[:limit]}
85
94
 
86
95
 
87
96
  @mcp.tool()
88
- def list_reports(symbol: str, doc_type: str = "annual", size: int = 10) -> dict:
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:
89
106
  """List a company's reports — metadata only (id, title, period), no body text.
90
107
 
91
- Args:
92
- symbol: FMP-style symbol, e.g. 600519.SS / 005930.KS / 7203.T
93
- doc_type: annual / semiannual / q1 / q3
94
- size: Number of reports to return (default 10).
108
+ Each item carries a ``source`` field naming the official disclosure platform;
109
+ keep that attribution when you cite it.
95
110
  """
96
111
  return _get("/documents", {"symbol": symbol, "doc_type": doc_type, "size": size})
97
112
 
98
113
 
99
114
  @mcp.tool()
100
- def get_report(document_id: int) -> dict:
115
+ def get_report(
116
+ document_id: Annotated[int, Field(description="Report id, from list_reports items[].id")],
117
+ ) -> dict:
101
118
  """Fetch a single report's full text (metadata + Markdown body).
102
119
 
103
- Args:
104
- document_id: Report id, from list_reports items[].id
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.
105
122
  """
106
123
  return _get(f"/documents/{document_id}")
107
124
 
108
125
 
109
126
  @mcp.tool()
110
- def list_sections(document_id: int) -> dict:
127
+ def list_sections(
128
+ document_id: Annotated[int, Field(description="Report id, from list_reports items[].id")],
129
+ ) -> dict:
111
130
  """List every section heading in a report (feed the headings to get_section).
112
131
 
113
- Args:
114
- document_id: Report id
132
+ Call this before get_section to see the exact headings — the headings are in the
133
+ report's own language.
115
134
  """
116
135
  return _get(f"/documents/{document_id}/sections")
117
136
 
118
137
 
119
138
  @mcp.tool()
120
- def get_section(document_id: int, section: str) -> dict:
121
- """Fetch only one section of a report by keyword — cheaper than get_report for RAG.
122
-
123
- Args:
124
- document_id: Report id
125
- section: Heading keyword, e.g. "management discussion" / "MD&A" / "financial statements" / "notes"
126
- """
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."""
127
156
  return _get(f"/documents/{document_id}", {"section": section})
128
157
 
129
158
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datasinking
3
- Version: 0.2.7
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"
File without changes
File without changes