databull 0.1.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.
databull/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ """databull —— DataBull 中国 A 股市场数据 API 的 Python SDK。
2
+
3
+ 快速开始::
4
+
5
+ pip install databull
6
+
7
+ from databull import DataBull
8
+
9
+ client = DataBull("dbt_xxx") # 也可用环境变量 DATABULL_API_KEY
10
+ df = client.get_stock_history("600519", "2026-01-01", "2026-09-01")
11
+ print(df.head())
12
+
13
+ 出错统一抛 :class:`DataBullError`(不静默返回 None);空结果的原因挂在
14
+ ``client.last_empty_reason``。全部可用端点见 README 的端点表。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from .client import DataBull, DataBullError, __version__
20
+
21
+ __all__ = ["DataBull", "DataBullError", "__version__"]
databull/__main__.py ADDED
@@ -0,0 +1,15 @@
1
+ """``python -m databull <endpoint> [--params JSON]`` —— 不写代码时快速探一个接口。
2
+
3
+ 示例::
4
+
5
+ python -m databull cn/stocks --params '{"q": "茅台"}'
6
+ python -m databull cn/index/search --params '{"keyword": "沪深300"}'
7
+ python -m databull --version
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .client import _cli
13
+
14
+ if __name__ == "__main__":
15
+ raise SystemExit(_cli())
databull/client.py ADDED
@@ -0,0 +1,745 @@
1
+ """DataBull Python SDK —— 中国 A 股市场数据 API 官方客户端。
2
+
3
+ 覆盖 DataBull 已上线并经生产验证的 28 个 REST 端点与 5 个 MCP 工具。
4
+ 鉴权:``Authorization: Bearer <Key>``(也接受 ``X-API-Key``);Key 在
5
+ https://www.databull.cn/console/register 免费注册后于控制台创建。
6
+ 基址默认 https://api.databull.cn,可用 ``base_url`` 覆盖。
7
+
8
+ 快速开始::
9
+
10
+ from databull import DataBull
11
+
12
+ client = DataBull("dbt_xxx") # 也可只设环境变量 DATABULL_API_KEY
13
+ df = client.get_stock_history("600519", "2026-01-01", "2026-09-01")
14
+
15
+ 设计上的三个要点:
16
+
17
+ 1. **出错显式抛 ``DataBullError``**,不静默返回 None —— 本项目反复出现「不报错但返回空」
18
+ 这类缺陷,把「请求失败」与「这段区间没数据」混在一起会让调用方误判。
19
+ 2. **空结果带原因**:响应头 ``X-Empty-Reason`` / 体内 ``empty_reason`` 会挂到
20
+ ``client.last_empty_reason``,用来区分「数据还没采到」与「代码写错了」。
21
+ 3. **参数先规整再发**:代码一律用裸码(600519 而不是 600519.SH),日期常见写法都收。
22
+ 服务端对这两件事都不严格一致——个股传后缀会 400、指数传后缀会**静默返回空数组**;
23
+ ``/cn/index/history`` 只认 ``YYYYMMDD`` 而其余端点两种都收。这些差异都在
24
+ ``_request`` 里统一抹平(``_normalize_symbol`` / ``_normalize_date``),
25
+ 新增方法不必各自记得转换。
26
+
27
+ 本文件是 SDK 的**唯一源**。分发给 WorkBuddy skill 的单文件版本
28
+ (``public/skill/databull_client.py``)由仓库根的 ``build_sdk.py`` 从本文件生成,
29
+ 请勿直接编辑那一份。
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import argparse
35
+ import json
36
+ import os
37
+ import re
38
+ import sys
39
+ from typing import Any, Dict, Optional
40
+
41
+ import requests
42
+ from requests.adapters import HTTPAdapter
43
+ from urllib3.util.retry import Retry
44
+
45
+ #: SDK 版本。**唯一源**:pyproject.toml 通过 dynamic 从本属性读取,
46
+ #: 不要再在别处硬编码版本号(testcase/test_databull_sdk.py 会校验一致性)。
47
+ __version__ = "0.1.0"
48
+
49
+ __all__ = ["DataBull", "DataBullError", "__version__"]
50
+
51
+
52
+ class DataBullError(Exception):
53
+ """DataBull API 调用错误,携带 HTTP 状态码与响应体。
54
+
55
+ :ivar status: HTTP 状态码;本地校验失败(未发请求)时为 0
56
+ :ivar body: 服务端原始响应体,可能为 None
57
+ """
58
+
59
+ def __init__(self, status: int, message: str, body: Optional[str] = None) -> None:
60
+ self.status = status
61
+ self.body = body
62
+ super().__init__(f"[{status}] {message}")
63
+
64
+
65
+ # 交易所后缀白名单:仅用于"剥离",绝不能误伤美股代码里的点(如 BRK.B)。
66
+ _EXCHANGE_SUFFIXES = {"SZ", "SH", "BJ", "SS", "XSHE", "XSHG"}
67
+
68
+ # 指数清单的「市场」是发布方维度,取值与作为基址路径前缀的 market=cn/us/hk
69
+ # 完全不同。两者名字都叫 market,混用**不会报错**,只会静默筛不到(空列表 +
70
+ # empty_reason=no_match),所以这里显式拦一道并给出可读提示。
71
+ _INDEX_MARKETS = frozenset({"SSE", "SZSE", "CSI", "SW"})
72
+ _PATH_MARKETS = frozenset({"CN", "US", "HK"})
73
+
74
+ # 常见日期写法:2026-08-01 / 2026/8/1 / 2026.8.1(见 _normalize_date)
75
+ _DATE_RE = re.compile(r"^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$")
76
+
77
+
78
+ def _normalize_symbol(symbol: str) -> str:
79
+ """把带交易所后缀的代码规整为 DataBull 接受的裸代码。
80
+
81
+ DataBull 的个股/ETF/指数接口(symbol / index_code 参数)都只接受裸代码,
82
+ 如 000001、600519、510300;市场由 URL 路径决定(/cn、/us、/hk),不在代码里标注。
83
+ 传入 000001.SZ 会被 validate_stock_code 拒绝并返回 400(长度超限 + 格式不符);
84
+ 指数传 000001.SH 更隐蔽——不报错但静默查不到数据(返回空数组)。
85
+
86
+ 本函数自动剥掉尾部 .SZ/.SH/.BJ/.SS 等后缀,但保留代码本身含有的点
87
+ (如美股 BRK.B),避免误伤。
88
+
89
+ :param symbol: 原始代码,可带后缀
90
+ :return: 裸代码;输入非字符串时原样返回
91
+ """
92
+ if not symbol or not isinstance(symbol, str):
93
+ return symbol
94
+ s = symbol.strip().upper()
95
+ if "." in s:
96
+ head, _, tail = s.rpartition(".")
97
+ if tail in _EXCHANGE_SUFFIXES:
98
+ return head
99
+ return s
100
+
101
+
102
+ def _normalize_date(value: Optional[str]) -> Optional[str]:
103
+ """把常见日期写法(2026-08-01 / 2026/8/1 / 2026.8.1)规整成 YYYYMMDD。
104
+
105
+ 服务端对日期的要求并不统一:实测只有 ``/cn/index/history`` 严格要求
106
+ ``YYYYMMDD``(传 2026-08-01 直接 400「日期格式错误,请使用YYYYMMDD格式」),
107
+ 其余带日期的端点两种写法都收。同一个 SDK 里不同方法要求不同写法是纯粹的坑,
108
+ 所以在这里统一收口:客户端一律接受常见写法,发请求时转成服务端要的格式。
109
+
110
+ 识别不出来的写法原样透传,让服务端给出它自己的错误信息(比客户端瞎猜好)。
111
+
112
+ :param value: 调用方传入的日期,可为空
113
+ :return: YYYYMMDD 形式的字符串;空值或无法识别时原样返回
114
+ """
115
+ if not value or not isinstance(value, str):
116
+ return value
117
+ m = _DATE_RE.match(value.strip())
118
+ if not m:
119
+ return value
120
+ year, month, day = m.groups()
121
+ return f"{year}{int(month):02d}{int(day):02d}"
122
+
123
+
124
+ def _normalize_index_market(value: Optional[str]) -> str:
125
+ """校验指数清单的市场过滤值,把「传了行情市场」这种常见误用显式拦下。
126
+
127
+ 服务端把这个参数叫 `market`,但它的取值是发布方(SSE/SZSE/CSI/SW),
128
+ 而不是行情基址里的 cn/us/hk。传错时服务端**不报错**,只返回空列表并带
129
+ empty_reason=no_match,很容易被当成「这个市场没有指数」。
130
+
131
+ :param value: 调用方传入的市场过滤值,可为空
132
+ :return: 规整后的过滤值(空串表示不过滤)
133
+ :raises DataBullError: 传了 cn/us/hk 等行情市场值,或不在允许取值内
134
+ """
135
+ v = (value or "").strip()
136
+ if not v:
137
+ return ""
138
+ if v.upper() in _PATH_MARKETS:
139
+ raise DataBullError(
140
+ 0,
141
+ f"index_market={value!r} 是行情市场(cn/us/hk),指数清单的市场过滤只接受 "
142
+ f"{'/'.join(sorted(_INDEX_MARKETS))};两者含义不同,留空表示不限。",
143
+ )
144
+ if v.upper() not in _INDEX_MARKETS:
145
+ raise DataBullError(
146
+ 0,
147
+ f"index_market={value!r} 不是合法的指数市场,可选:"
148
+ f"{'/'.join(sorted(_INDEX_MARKETS))};留空表示不限。",
149
+ )
150
+ return v.upper()
151
+
152
+
153
+ class DataBull:
154
+ """DataBull 数据 API 客户端。
155
+
156
+ :param api_key: API Key;留空则读环境变量 ``DATABULL_API_KEY``
157
+ :param base_url: 覆盖服务基址,默认 https://api.databull.cn
158
+ :param timeout: 单次请求超时秒数,默认 15
159
+ :raises DataBullError: 未提供任何 API Key 时立即抛出(不等到第一次请求才失败)
160
+ """
161
+
162
+ DEFAULT_BASE_URL = "https://api.databull.cn"
163
+
164
+ # 合法的 HTTP 方法,用于区分 _request 的两种调用风格(见 _request 注释)
165
+ _HTTP_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"})
166
+
167
+ def __init__(
168
+ self,
169
+ api_key: Optional[str] = None,
170
+ base_url: Optional[str] = None,
171
+ timeout: int = 15,
172
+ ) -> None:
173
+ self.api_key = api_key or os.getenv("DATABULL_API_KEY")
174
+ if not self.api_key:
175
+ raise DataBullError(
176
+ 0,
177
+ "缺少 API Key:请设置环境变量 DATABULL_API_KEY,或在构造时传入 api_key。",
178
+ )
179
+ self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
180
+ self.timeout = timeout
181
+ self._last_empty_reason: Optional[str] = None
182
+ self.session = self._build_session()
183
+ self.session.headers.update(
184
+ {
185
+ "Authorization": f"Bearer {self.api_key}",
186
+ "Accept": "application/json",
187
+ "Content-Type": "application/json",
188
+ }
189
+ )
190
+
191
+ def __repr__(self) -> str:
192
+ # 不回显 api_key:__repr__ 常出现在日志与 traceback 里
193
+ return f"DataBull(base_url={self.base_url!r}, version={__version__!r})"
194
+
195
+ # -------------------------- 底层请求 --------------------------
196
+ def _build_session(self) -> requests.Session:
197
+ session = requests.Session()
198
+ adapter = HTTPAdapter(pool_connections=20, pool_maxsize=20)
199
+ session.mount("https://", adapter)
200
+ session.mount("http://", adapter)
201
+ retry = Retry(
202
+ total=3,
203
+ backoff_factor=1,
204
+ status_forcelist=[429, 500, 502, 503, 504],
205
+ respect_retry_after_header=True,
206
+ raise_on_status=False,
207
+ )
208
+ session.mount("https://", HTTPAdapter(max_retries=retry))
209
+ return session
210
+
211
+ def _request(
212
+ self,
213
+ method: str,
214
+ endpoint: Optional[Any] = None,
215
+ params: Optional[Dict[str, Any]] = None,
216
+ json_body: Optional[Dict[str, Any]] = None,
217
+ ) -> Any:
218
+ """发一次请求并返回解析后的 JSON。
219
+
220
+ 本类的方法有两种写法,两者都必须能跑(曾因删掉这段兜底,
221
+ 导致分发出去的客户端除 get_index_history/call_mcp_tool 外**全部**在
222
+ 第一行就 AttributeError: 'NoneType' object has no attribute 'lstrip')::
223
+
224
+ _request("cn/index/history", params=...) # 只给路径,方法默认 GET
225
+ _request("POST", "/mcp/call", json_body=...) # 显式给方法
226
+
227
+ 判定方式:第二个参数为空、或首参不是合法 HTTP 方法,就把首参当路径。
228
+ 另外兼容「旧客户端的 _request(endpoint, params)」写法,即第二个位置参数
229
+ 直接是参数字典。
230
+
231
+ :param method: HTTP 方法,或(简写风格下)请求路径
232
+ :param endpoint: 请求路径;简写风格下留空
233
+ :param params: 查询参数
234
+ :param json_body: POST 体
235
+ :return: 响应 JSON(dict 或 list)
236
+ :raises DataBullError: 超时 / 连接失败 / 非 2xx / 响应非 JSON
237
+ """
238
+ if isinstance(endpoint, dict) and params is None:
239
+ # 旧签名 _request(endpoint, params):第二个位置参数是查询参数
240
+ method, endpoint, params = "GET", method, endpoint
241
+ elif endpoint is None or str(method).upper() not in self._HTTP_METHODS:
242
+ method, endpoint = "GET", method
243
+ if not isinstance(endpoint, str) or not endpoint.strip():
244
+ raise DataBullError(0, "请求路径为空")
245
+
246
+ url = self.base_url.rstrip("/") + "/" + endpoint.lstrip("/")
247
+ if params:
248
+ params = dict(params)
249
+ # symbol 与 index_code 都只接受裸码:个股/ETF 传后缀会被拒 400,
250
+ # 指数传后缀不报错但静默返回空数组。统一在此剥离后缀。
251
+ for key in ("symbol", "index_code"):
252
+ if isinstance(params.get(key), str):
253
+ params[key] = _normalize_symbol(params[key])
254
+ # 日期写法也在此统一:服务端只有 /cn/index/history 严格要求 YYYYMMDD,
255
+ # 其余端点两种都收。集中规整,新增方法不必各自记得转换。
256
+ for key in ("start_date", "end_date"):
257
+ if isinstance(params.get(key), str):
258
+ params[key] = _normalize_date(params[key])
259
+ try:
260
+ resp = self.session.request(
261
+ method, url, params=params, json=json_body, timeout=self.timeout
262
+ )
263
+ except requests.exceptions.Timeout:
264
+ raise DataBullError(0, "请求超时")
265
+ except requests.exceptions.ConnectionError:
266
+ raise DataBullError(0, "网络连接失败")
267
+
268
+ if resp.status_code == 429:
269
+ retry_after = resp.headers.get("Retry-After")
270
+ msg = "调用频率超限(429)"
271
+ if retry_after:
272
+ msg += f",建议 {retry_after}s 后重试"
273
+ raise DataBullError(429, msg, resp.text)
274
+ if not resp.ok:
275
+ raise DataBullError(resp.status_code, f"HTTP 错误:{resp.text[:300]}", resp.text)
276
+
277
+ try:
278
+ payload = resp.json()
279
+ except ValueError:
280
+ raise DataBullError(resp.status_code, "响应不是合法 JSON", resp.text)
281
+
282
+ # 服务端对「空结果」会在响应头 X-Empty-Reason(数组型响应)或体内
283
+ # empty_reason(对象型响应)里给出原因。把它挂到返回对象上,调用方
284
+ # 就不会把「数据还没采到 / 代码写错了」误当成「这个标的没行情」。
285
+ self._last_empty_reason = resp.headers.get("X-Empty-Reason")
286
+ if not self._last_empty_reason and isinstance(payload, dict):
287
+ self._last_empty_reason = payload.get("empty_reason")
288
+ return payload
289
+
290
+ @property
291
+ def last_empty_reason(self) -> Optional[str]:
292
+ """最近一次请求为空结果时的原因(None 表示最近一次请求非空/无标注)。"""
293
+ return getattr(self, "_last_empty_reason", None)
294
+
295
+ @staticmethod
296
+ def _to_dataframe(data: Any, date_col: str = "date"):
297
+ """把接口返回的 JSON 转成带日期索引的 DataFrame。
298
+
299
+ :param data: 接口 data 字段(list 或 dict)
300
+ :param date_col: 用作索引的日期列名
301
+ :return: DataFrame;数据为空或类型不符时返回 None
302
+ :raises ImportError: 未安装 pandas(它是可选依赖:``pip install databull[pandas]``)
303
+ """
304
+ try:
305
+ import pandas as pd # 延迟导入:无 pandas 环境仍可用全部返回 JSON 的方法
306
+ except ImportError as e: # 给出可直接照做的提示,而不是裸的 ModuleNotFoundError
307
+ raise ImportError(
308
+ "该方法返回 DataFrame,需要 pandas:pip install 'databull[pandas]'"
309
+ ) from e
310
+
311
+ if not data:
312
+ return None
313
+ if isinstance(data, (list, dict)):
314
+ df = pd.DataFrame(data)
315
+ if df.empty:
316
+ return None
317
+ if date_col in df.columns:
318
+ df[date_col] = pd.to_datetime(df[date_col])
319
+ df.set_index(date_col, inplace=True)
320
+ return df
321
+ return None
322
+
323
+ # -------------------------- 指数 --------------------------
324
+ def get_index_history(
325
+ self, index_code: str, start_date: str, end_date: str, market: str = "cn"
326
+ ):
327
+ """指数历史日线行情。
328
+
329
+ :param index_code: 指数代码(裸码,如 000300;传 000300.SH 也能自动剥离)
330
+ :param start_date: 起始日,YYYY-MM-DD / YYYYMMDD 都可以(本端点服务端只认
331
+ YYYYMMDD,客户端会自动转换;留空表示不限)
332
+ :param end_date: 结束日,同上
333
+ :param market: 基址路径前缀(cn/us/hk),默认 cn
334
+ :return: 带日期索引的 DataFrame
335
+ """
336
+ data = self._request(
337
+ "GET",
338
+ f"{market}/index/history",
339
+ params={"index_code": index_code, "start_date": start_date, "end_date": end_date},
340
+ )
341
+ return self._to_dataframe(data.get("data") if isinstance(data, dict) else data)
342
+
343
+ def get_index_list(
344
+ self,
345
+ index_market: str = "",
346
+ category: str = "",
347
+ keyword: str = "",
348
+ page: int = 1,
349
+ page_size: int = 100,
350
+ market: str = "cn",
351
+ ):
352
+ """指数清单分页查询(平台收录了哪些指数)。
353
+
354
+ :param index_market: **指数市场过滤**,SSE 上交所 / SZSE 深交所 / CSI 中证 /
355
+ SW 申万;留空表示不限
356
+ :param category: 分类过滤,如 规模指数 / 行业指数 / 主题指数 / 策略指数;
357
+ 须与库中取值完全一致,留空表示不限
358
+ :param keyword: 关键词,同时匹配名称、裸码与全码(沪深300 / 000300)
359
+ :param page: 页码,从 1 开始
360
+ :param page_size: 每页条数,1~500
361
+ :param market: 基址路径前缀(cn/us/hk),默认 cn —— 注意它**不是**市场过滤,
362
+ 市场过滤请用 index_market
363
+ :return: {'code': 0, 'data': {'items': [...], 'total': int, 'page': int,
364
+ 'page_size': int, 'has_more': bool}},空结果额外带 'empty_reason'
365
+ (catalog_not_synced = 清单还没同步;no_match = 条件太窄没筛到)
366
+ """
367
+ params: Dict[str, Any] = {"page": page, "page_size": page_size}
368
+ checked = _normalize_index_market(index_market)
369
+ if checked:
370
+ params["market"] = checked
371
+ if category:
372
+ params["category"] = category
373
+ if keyword:
374
+ params["keyword"] = keyword
375
+ return self._request(f"{market}/index/list", params=params)
376
+
377
+ def search_index(
378
+ self, keyword: str, limit: int = 20, index_market: str = "", market: str = "cn"
379
+ ):
380
+ """按关键词搜索指数,按相关度排序(适合输入框联想)。
381
+
382
+ 相关度:裸码/全码完全相等 > 裸码前缀 > 名称前缀 > 名称包含 > 代码包含。
383
+ 与 get_index_list(keyword=...) 的区别:本方法 keyword 必填且按**相关度**排序,
384
+ 列表接口则是按代码升序的严格过滤,两者互补。
385
+
386
+ :param keyword: 搜索关键词(必填):名称、裸码或全码,如 沪深300 / 000300 / 000300.SH
387
+ :param limit: 返回条数上限,1~100
388
+ :param index_market: 指数市场过滤 SSE / SZSE / CSI / SW;留空不限
389
+ :param market: 基址路径前缀(cn/us/hk),默认 cn;市场过滤请用 index_market
390
+ :return: {'code': 0, 'data': [...]},没搜到时 data 为空数组且 empty_reason=no_match
391
+ :raises DataBullError: keyword 为空时(本地拦下,不发请求)
392
+ """
393
+ if not keyword or not str(keyword).strip():
394
+ raise DataBullError(0, "keyword 必填:请传指数名称、裸码或全码")
395
+ params: Dict[str, Any] = {"keyword": keyword, "limit": limit}
396
+ checked = _normalize_index_market(index_market)
397
+ if checked:
398
+ params["market"] = checked
399
+ return self._request(f"{market}/index/search", params=params)
400
+
401
+ def get_index_constituents(
402
+ self,
403
+ index_code: str = "000300",
404
+ page: int = 1,
405
+ page_size: int = 100,
406
+ market: str = "cn",
407
+ ):
408
+ """指数成分股及权重(按权重降序,最新一期快照)。
409
+
410
+ 注意成分股**每天只同步主流宽基与常用行业主题指数**,不在同步名单里的指数
411
+ 返回空并带 empty_reason=constituents_not_synced。
412
+
413
+ :param index_code: 指数代码,裸码 000300 或全码 000300.SH 都可以
414
+ :param page: 页码,从 1 开始
415
+ :param page_size: 每页条数,1~5000
416
+ :param market: 基址路径前缀(cn/us/hk),默认 cn
417
+ :return: {'code': 0, 'data': {'items': [...], 'total': int, 'page': int,
418
+ 'page_size': int, 'has_more': bool, 'trade_date': str|None,
419
+ 'index': {命中的指数档案}}};空结果带 empty_reason
420
+ (constituents_not_synced = 该指数不在每日同步名单;
421
+ index_not_found = 清单里没这只指数)
422
+ """
423
+ return self._request(
424
+ f"{market}/index/constituents",
425
+ params={"index_code": index_code, "page": page, "page_size": page_size},
426
+ )
427
+
428
+ # -------------------------- A股 个股 --------------------------
429
+ def get_stock_list(
430
+ self,
431
+ market: str = "cn",
432
+ q: Optional[str] = None,
433
+ exchange: Optional[str] = None,
434
+ search: Optional[str] = None,
435
+ ):
436
+ """A股/ETF/指数标的清单,支持关键词与交易所过滤。
437
+
438
+ :param market: 基址路径前缀,默认 cn
439
+ :param q: 关键词
440
+ :param exchange: 交易所 SH/SZ
441
+ :param search: 【deprecated】旧客户端的参数名,等价于 q,保留以兼容既有调用点
442
+ :return: {'code': 0, 'data': [...]}
443
+ """
444
+ params: Dict[str, Any] = {}
445
+ keyword = q or search
446
+ if keyword:
447
+ params["q"] = keyword
448
+ if exchange:
449
+ params["exchange"] = exchange
450
+ return self._request(f"{market}/stocks", params=params or None)
451
+
452
+ def get_stock_info(self, symbol: str, market: str = "cn"):
453
+ """个股基本信息(symbol/name/market/float_volume/total_volume)。"""
454
+ return self._request(f"{market}/stock/info", params={"symbol": symbol})
455
+
456
+ def get_company_profile(self, symbol: str, market: str = "cn"):
457
+ """公司简介。"""
458
+ return self._request(f"{market}/stock/profile", params={"symbol": symbol})
459
+
460
+ def get_stock_history(
461
+ self,
462
+ symbol: str,
463
+ start_date: str,
464
+ end_date: str,
465
+ period: str = "d",
466
+ market: str = "cn",
467
+ ):
468
+ """个股历史行情(日线/分钟线),返回带日期索引的 DataFrame。"""
469
+ data = self._request(
470
+ f"{market}/stock/history",
471
+ params={
472
+ "symbol": symbol,
473
+ "start_date": start_date,
474
+ "end_date": end_date,
475
+ "period": period,
476
+ "v": "1.0",
477
+ },
478
+ )
479
+ return self._to_dataframe(data.get("data") if isinstance(data, dict) else data)
480
+
481
+ def get_stock_financial_data(
482
+ self,
483
+ symbol: str,
484
+ start_date: str,
485
+ end_date: str,
486
+ report_type: str = "Balance",
487
+ ):
488
+ """个股财报:report_type ∈ Balance/Income/CashFlow/Capital。"""
489
+ return self._request(
490
+ "cn/stock/financial_data",
491
+ params={
492
+ "symbol": symbol,
493
+ "start_date": start_date,
494
+ "end_date": end_date,
495
+ "report_type": report_type,
496
+ },
497
+ )
498
+
499
+ # -------------------------- 美股 / 港股 --------------------------
500
+ def get_us_stock_info(self, symbol: str):
501
+ """美股个股基本信息。"""
502
+ return self._request("us/stock/info", params={"symbol": symbol})
503
+
504
+ def get_us_stock_history(
505
+ self, symbol: str, start_date: str, end_date: str, period: str = "d"
506
+ ):
507
+ """美股历史行情,返回带日期索引的 DataFrame。"""
508
+ data = self._request(
509
+ "us/stock/history",
510
+ params={
511
+ "symbol": symbol,
512
+ "start_date": start_date,
513
+ "end_date": end_date,
514
+ "period": period,
515
+ },
516
+ )
517
+ return self._to_dataframe(data.get("data") if isinstance(data, dict) else data)
518
+
519
+ def get_hk_stock_info(self, symbol: str):
520
+ """港股个股基本信息。"""
521
+ return self._request("hk/stock/info", params={"symbol": symbol})
522
+
523
+ def get_hk_stock_history(
524
+ self, symbol: str, start_date: str, end_date: str, period: str = "d"
525
+ ):
526
+ """港股历史行情,返回带日期索引的 DataFrame。"""
527
+ data = self._request(
528
+ "hk/stock/history",
529
+ params={
530
+ "symbol": symbol,
531
+ "start_date": start_date,
532
+ "end_date": end_date,
533
+ "period": period,
534
+ },
535
+ )
536
+ return self._to_dataframe(data.get("data") if isinstance(data, dict) else data)
537
+
538
+ # -------------------------- ETF --------------------------
539
+ def get_etf_list(
540
+ self,
541
+ market: str = "cn",
542
+ q: Optional[str] = None,
543
+ exchange: Optional[str] = None,
544
+ search: Optional[str] = None,
545
+ ):
546
+ """ETF 清单,支持关键词与交易所过滤。
547
+
548
+ :param search: 【deprecated】旧客户端的参数名,等价于 q
549
+ """
550
+ params: Dict[str, Any] = {}
551
+ keyword = q or search
552
+ if keyword:
553
+ params["q"] = keyword
554
+ if exchange:
555
+ params["exchange"] = exchange
556
+ return self._request(f"{market}/etfs", params=params or None)
557
+
558
+ def get_etf_info(self, symbol: str, market: str = "cn"):
559
+ """ETF 基本信息。"""
560
+ res = self._request(f"{market}/etfs/info", params={"symbol": symbol})
561
+ return res.get("data") if isinstance(res, dict) else res
562
+
563
+ def get_etf_pcf(self, symbol: str, market: str = "cn"):
564
+ """ETF 申赎清单(PCF)。"""
565
+ return self._request(f"{market}/etfs/etf_pcf", params={"symbol": symbol})
566
+
567
+ def get_etf_composition(self, symbol: str, market: str = "cn"):
568
+ """ETF 成分股。
569
+
570
+ 注意路径是 ``/{market}/etfs/etf_composition``:旧客户端的
571
+ ``/{market}/etf_composition`` 少一层 ``etfs``,线上是 404。
572
+ """
573
+ return self._request(f"{market}/etfs/etf_composition", params={"symbol": symbol})
574
+
575
+ def get_etf_history(self, symbol: str, start_date: str, end_date: str, market: str = "cn"):
576
+ """ETF 历史净值/行情,返回带日期索引的 DataFrame。"""
577
+ data = self._request(
578
+ f"{market}/etf/history",
579
+ params={"symbol": symbol, "start_date": start_date, "end_date": end_date},
580
+ )
581
+ return self._to_dataframe(data.get("data") if isinstance(data, dict) else data)
582
+
583
+ # -------------------------- 新闻 --------------------------
584
+ def get_news_flash(self, sources: Optional[str] = None, limit: Optional[int] = None):
585
+ """财经快讯(财联社/金十/新浪三源)。sources 逗号分隔过滤来源。"""
586
+ params: Dict[str, Any] = {}
587
+ if sources:
588
+ params["sources"] = sources
589
+ if limit:
590
+ params["limit"] = limit
591
+ return self._request("v2/news/flash", params=params or None)
592
+
593
+ def get_news_flash_sources(self):
594
+ """财经快讯可用来源列表。"""
595
+ return self._request("v2/news/flash/sources")
596
+
597
+ def get_market_news(self, page: int = 1, page_size: int = 20, market: str = "cn"):
598
+ """市场新闻列表。"""
599
+ return self._request(
600
+ f"{market}/market/news", params={"page": page, "page_size": page_size}
601
+ )
602
+
603
+ def get_market_news_detail(self, news_id: str, market: str = "cn"):
604
+ """市场新闻详情。"""
605
+ return self._request(f"{market}/market/news/{news_id}")
606
+
607
+ # -------------------------- 市场 / 板块 --------------------------
608
+ def get_fear_greed(self, index_code: str, start_date: str, end_date: str, market: str = "cn"):
609
+ """恐惧与贪婪指数日线,返回带日期索引的 DataFrame。"""
610
+ data = self._request(
611
+ f"{market}/market/fear_greed",
612
+ params={"index_code": index_code, "start_date": start_date, "end_date": end_date},
613
+ )
614
+ return self._to_dataframe(data.get("data") if isinstance(data, dict) else data)
615
+
616
+ def get_sector_data(self, sector_type: str, market: str = "cn"):
617
+ """申万板块涨跌幅。sector_type 仅接受 sw1/sw2/sw3(小写,不区分大小写)。"""
618
+ return self._request(f"{market}/market/sector_data/{sector_type}")
619
+
620
+ def get_sector_catalog(self, sector_type: str, market: str = "cn"):
621
+ """申万行业板块清单(静态,每周从 Tushare SW2021 同步)。sector_type ∈ sw1/sw2/sw3。"""
622
+ return self._request(f"{market}/market/sector_catalog/{sector_type}")
623
+
624
+ # -------------------------- 实时 / Tick --------------------------
625
+ def get_realtime(self, symbol: str, tick_type: str = "stock", market: str = "cn"):
626
+ """个股实时 Tick / 日内分时。"""
627
+ return self._request(
628
+ f"{market}/stock/tick", params={"symbol": symbol, "tick_type": tick_type}
629
+ )
630
+
631
+ def get_tick_all(self, tick_type: str = "stock", market: str = "cn"):
632
+ """全量 Tick 数据包。
633
+
634
+ tick_type ∈ stock / etf / index —— 采集端按类型分别写入快照,
635
+ 读取时必须带上类型,否则键不匹配会恒定拿到空对象。
636
+ """
637
+ return self._request(f"{market}/tick/tickall", params={"tick_type": tick_type})
638
+
639
+ def get_trading_calendar(
640
+ self,
641
+ market: str = "cn",
642
+ start_date: Optional[str] = None,
643
+ end_date: Optional[str] = None,
644
+ year: Optional[int] = None,
645
+ ):
646
+ """交易日历,返回 ISO 日期字符串数组。"""
647
+ params: Dict[str, Any] = {}
648
+ if market and market != "cn":
649
+ params["market"] = market
650
+ if start_date:
651
+ params["start_date"] = start_date
652
+ if end_date:
653
+ params["end_date"] = end_date
654
+ if year is not None:
655
+ params["year"] = year
656
+ return self._request(f"{market}/tick/trading_calendar", params=params or None)
657
+
658
+ # -------------------------- MCP 工具接口(托管 MCP 服务用)--------------------------
659
+ def call_mcp_tool(self, function_name: str, parameters: Optional[Dict[str, Any]] = None):
660
+ """调用托管 MCP 工具(/mcp/call)。已暴露工具见 references/endpoints.md。"""
661
+ params = dict(parameters or {})
662
+ # 同 _request:symbol / index_code 都规整为裸码
663
+ for key in ("symbol", "index_code"):
664
+ if isinstance(params.get(key), str):
665
+ params[key] = _normalize_symbol(params[key])
666
+ return self._request(
667
+ "POST", "/mcp/call", json_body={"function_name": function_name, "parameters": params}
668
+ )
669
+
670
+ # -------------------------- 兼容旧方法名(deprecated) --------------------------
671
+ # 仓库内旧客户端 service/databull_api.py 用的方法名与上面不同,为不打断既有
672
+ # 调用点(utils/beta_calculate.py 等),这里保留同名转发。新代码请直接用上面
673
+ # 的新名字;这几个别名将来会移除。
674
+ # 注意:旧客户端出错是「print 后返回 None」,这里是抛 DataBullError —— 迁移时
675
+ # 调用方若依赖 None 分支,要改成 except DataBullError。
676
+
677
+ def get_history(
678
+ self, symbol: str, start_date: str, end_date: str, period: str = "d", market: str = "cn"
679
+ ):
680
+ """【deprecated】旧名,请改用 :meth:`get_stock_history`。"""
681
+ return self.get_stock_history(
682
+ symbol, start_date, end_date, period=period, market=market
683
+ )
684
+
685
+ def get_market_sector(self, sector_type: str = "sw1", market: str = "cn"):
686
+ """【deprecated】旧名,请改用 :meth:`get_sector_data`。"""
687
+ return self.get_sector_data(sector_type, market=market)
688
+
689
+ def get_company(self, symbol: str, market: str = "cn"):
690
+ """【deprecated】旧名,请改用 :meth:`get_company_profile`。
691
+
692
+ 与旧实现的差异:旧版把响应里的 ``data`` 取出来再返回,本别名与其他方法
693
+ 一致返回完整响应体(``{'code': 0, 'data': {...}}``)。
694
+ """
695
+ return self.get_company_profile(symbol, market=market)
696
+
697
+ def get_last_tick(self, symbol: str, tick_type: str = "stock", market: str = "cn"):
698
+ """【deprecated】旧名,请改用 :meth:`get_realtime`。"""
699
+ return self.get_realtime(symbol, tick_type=tick_type, market=market)
700
+
701
+ def get_market_fear_greed(
702
+ self, index_code: str, start_date: str, end_date: str, market: str = "cn"
703
+ ):
704
+ """【deprecated】旧名,请改用 :meth:`get_fear_greed`。"""
705
+ return self.get_fear_greed(index_code, start_date, end_date, market=market)
706
+
707
+
708
+ def _cli() -> int:
709
+ """``python -m databull`` / 单文件版的命令行入口。"""
710
+ parser = argparse.ArgumentParser(
711
+ prog="databull", description="DataBull API 命令行客户端"
712
+ )
713
+ parser.add_argument("endpoint", nargs="?", help="端点路径,如 cn/stock/info")
714
+ parser.add_argument("--params", help="JSON 字符串,作为查询参数或 POST body")
715
+ parser.add_argument("--method", default="GET", help="HTTP 方法,默认 GET")
716
+ parser.add_argument("--base-url", default=None, help="覆盖基址")
717
+ parser.add_argument("--api-key", default=None, help="覆盖 DATABULL_API_KEY")
718
+ parser.add_argument("--version", action="store_true", help="打印 SDK 版本后退出")
719
+ args = parser.parse_args()
720
+
721
+ if args.version:
722
+ print(f"databull {__version__}")
723
+ return 0
724
+ if not args.endpoint:
725
+ parser.print_help()
726
+ return 2
727
+
728
+ try:
729
+ client = DataBull(api_key=args.api_key, base_url=args.base_url)
730
+ params = json.loads(args.params) if args.params else None
731
+ if args.method.upper() == "GET":
732
+ result = client._request("GET", args.endpoint, params=params)
733
+ else:
734
+ result = client._request(args.method.upper(), args.endpoint, json_body=params)
735
+ print(json.dumps(result, ensure_ascii=False, indent=2))
736
+ return 0
737
+ except DataBullError as e:
738
+ print(f"DataBullError: {e}", file=sys.stderr)
739
+ if e.body:
740
+ print(e.body, file=sys.stderr)
741
+ return 1
742
+
743
+
744
+ if __name__ == "__main__":
745
+ raise SystemExit(_cli())
databull/py.typed ADDED
File without changes
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: databull
3
+ Version: 0.1.0
4
+ Summary: DataBull 中国 A 股市场数据 API 官方 Python SDK(行情、财务、ETF、指数、板块、快讯)
5
+ Author-email: Yc <yccheni@163.com>
6
+ Project-URL: Homepage, https://www.databull.cn
7
+ Project-URL: Documentation, https://www.databull.cn/docs
8
+ Project-URL: Source, https://www.databull.cn/sdk
9
+ Project-URL: API Console, https://www.databull.cn/console
10
+ Keywords: databull,stock,a-share,china,finance,market-data,api,etf,index
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Office/Business :: Financial :: Investment
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.28
22
+ Requires-Dist: urllib3>=1.26
23
+ Provides-Extra: pandas
24
+ Requires-Dist: pandas>=1.5; extra == "pandas"
25
+
26
+ # databull — DataBull Python SDK
27
+
28
+ DataBull 中国 A 股市场数据 API 的官方 Python 客户端。覆盖**沪深个股日线与实时行情、财务报表、
29
+ ETF(含申赎清单与成分股)、指数清单/搜索/成分股权重、申万板块、市场情绪、财经快讯**,
30
+ 共 28 个 REST 端点与 5 个 MCP 工具。
31
+
32
+ - 官网与文档:https://www.databull.cn/docs
33
+ - 免费注册并创建 API Key:https://www.databull.cn/console/register
34
+
35
+ ## 安装
36
+
37
+ ```bash
38
+ pip install databull # 仅 JSON 查询,依赖只有 requests
39
+ pip install "databull[pandas]" # 需要 *_history / fear_greed 返回 DataFrame 时
40
+ ```
41
+
42
+ 国内直连 PyPI 慢时,可从官网下载 wheel 安装:
43
+
44
+ ```bash
45
+ pip install https://www.databull.cn/sdk/files/databull-0.1.0-py3-none-any.whl
46
+ ```
47
+
48
+ ## 快速开始
49
+
50
+ ```python
51
+ from databull import DataBull, DataBullError
52
+
53
+ client = DataBull("dbt_xxxxxxxx") # 也可只设环境变量 DATABULL_API_KEY
54
+ # 自建/代理时可指定基址:DataBull(api_key, base_url="https://api.example.com")
55
+
56
+ # 个股日线(返回带日期索引的 DataFrame)
57
+ df = client.get_stock_history("600519", "2026-01-01", "2026-09-01")
58
+ print(df.tail())
59
+
60
+ # 指数搜索与成分股权重
61
+ hits = client.search_index("沪深300", limit=5)
62
+ cons = client.get_index_constituents("000300", page_size=10)["data"]
63
+
64
+ # 申万一级行业涨跌
65
+ sectors = client.get_sector_data("sw1")
66
+ ```
67
+
68
+ 命令行也可以直接探接口(不用写代码):
69
+
70
+ ```bash
71
+ export DATABULL_API_KEY=dbt_xxxxxxxx
72
+ python -m databull cn/index/search --params '{"keyword": "沪深300"}'
73
+ databull cn/stocks --params '{"q": "茅台"}' # 安装了就绪脚本
74
+ python -m databull --version
75
+ ```
76
+
77
+ ## 出错与空结果
78
+
79
+ **出错不静默**:所有失败都抛 `DataBullError`,带上 HTTP 状态码与原始响应体。
80
+ 不要用「返回 None」判断失败——本项目历史上就吃过这个亏。
81
+
82
+ ```python
83
+ try:
84
+ client.get_stock_history("600519", "2026-01-01", "2026-09-01")
85
+ except DataBullError as e:
86
+ print(e.status, e) # 401=Key 无效 403=超出套餐额度 429=调用频率超限
87
+ print(e.body)
88
+ ```
89
+
90
+ **空结果带原因**:接口在「没有数据」时返回空数组/空对象而不是报错,原因通过响应头
91
+ `X-Empty-Reason`(数组型)或体内 `empty_reason`(对象型)给出,SDK 把它挂到
92
+ `client.last_empty_reason`:
93
+
94
+ ```python
95
+ data = client.get_index_constituents("801010")
96
+ if not data["data"]["items"]:
97
+ print(client.last_empty_reason) # constituents_not_synced = 该指数不在每日同步名单
98
+ ```
99
+
100
+ 常见取值:`index_not_found`、`no_data_in_range`、`calendar_not_loaded`、`no_match`、
101
+ `sector_data_not_collected`、`tick_not_uploaded`、`catalog_not_synced`、
102
+ `constituents_not_synced`。
103
+
104
+ ## 参数与返回约定(容易踩的两点)
105
+
106
+ 1. **代码一律用裸码**,不带交易所后缀:`600519` 而不是 `600519.SH`;市场由接口决定。
107
+ 传后缀时个股会 400,指数**不报错但静默返回空数组**——SDK 已自动剥离后缀,
108
+ 但你自己拼 URL 时要注意。
109
+ 2. **日期写法都收**:`2026-08-01`、`20260801`、`2026/8/1` 都可以。服务端并不统一——
110
+ 实测只有 `/cn/index/history` 严格要求 `YYYYMMDD`(传 `2026-08-01` 直接 400),
111
+ 其余端点两种都收;SDK 在发请求前统一转换,所以你不必记这件事。
112
+ 3. **`market` 有两种含义**:作为基址路径前缀时是 `cn` / `us` / `hk`;作为**指数清单的
113
+ 过滤条件**时是 `SSE` / `SZSE` / `CSI` / `SW`。两者混用不会报错、只会筛不到。
114
+ SDK 把后者命名为 `index_market` 并会提前拦下错误取值。
115
+
116
+ **返回形态不统一**:多数方法返回 `{"code": 0, "data": ...}`,但少数接口直接返回数组
117
+ (例如 `get_sector_data` 返回 `[{...}, ...]`)。SDK 原样透传,不做包装——
118
+ 按方法逐个确认一次,比被一层"透明"包装误导更好。
119
+
120
+ ## 端点与方法对照
121
+
122
+ | 能力 | 方法 | 路径 |
123
+ | --- | --- | --- |
124
+ | 指数历史日线 | `get_index_history` | `GET /cn/index/history` |
125
+ | 指数清单(分页/过滤) | `get_index_list` | `GET /cn/index/list` |
126
+ | 指数搜索(相关度排序) | `search_index` | `GET /cn/index/search` |
127
+ | 指数成分股与权重 | `get_index_constituents` | `GET /cn/index/constituents` |
128
+ | A股/ETF/指数标的清单 | `get_stock_list` | `GET /cn/stocks` |
129
+ | 个股基本信息 | `get_stock_info` | `GET /cn/stock/info` |
130
+ | 公司简介 | `get_company_profile` | `GET /cn/stock/profile` |
131
+ | 个股历史行情 | `get_stock_history` | `GET /cn/stock/history` |
132
+ | 个股财务报表 | `get_stock_financial_data` | `GET /cn/stock/financial_data` |
133
+ | 美股基本信息 / 历史 | `get_us_stock_info` / `get_us_stock_history` | `GET /us/stock/info`, `/us/stock/history` |
134
+ | 港股基本信息 / 历史 | `get_hk_stock_info` / `get_hk_stock_history` | `GET /hk/stock/info`, `/hk/stock/history` |
135
+ | ETF 清单 | `get_etf_list` | `GET /cn/etfs/` |
136
+ | ETF 基本信息 | `get_etf_info` | `GET /cn/etfs/info` |
137
+ | ETF 申赎清单(PCF) | `get_etf_pcf` | `GET /cn/etfs/etf_pcf` |
138
+ | ETF 成分股 | `get_etf_composition` | `GET /cn/etfs/etf_composition` |
139
+ | ETF 历史净值/行情 | `get_etf_history` | `GET /cn/etf/history` |
140
+ | 财经快讯 | `get_news_flash` | `GET /v2/news/flash` |
141
+ | 快讯来源列表 | `get_news_flash_sources` | `GET /v2/news/flash/sources` |
142
+ | 市场新闻列表 / 详情 | `get_market_news` / `get_market_news_detail` | `GET /cn/market/news`, `/cn/market/news/{id}` |
143
+ | 恐惧与贪婪指数 | `get_fear_greed` | `GET /cn/market/fear_greed` |
144
+ | 申万板块涨跌幅 | `get_sector_data` | `GET /cn/market/sector_data/{sw1,sw2,sw3}` |
145
+ | 申万行业清单 | `get_sector_catalog` | `GET /cn/market/sector_catalog/{sw1,sw2,sw3}` |
146
+ | 个股实时 Tick | `get_realtime` | `GET /cn/stock/tick` |
147
+ | 全量 Tick 快照 | `get_tick_all` | `GET /cn/tick/tickall` |
148
+ | 交易日历 | `get_trading_calendar` | `GET /cn/tick/trading_calendar` |
149
+ | MCP 工具调用 | `call_mcp_tool` | `POST /mcp/call` |
150
+
151
+ MCP 已暴露的工具:`get_stock_list`、`get_stock_detail`、`get_stock_history`、
152
+ `get_index_history`、`get_financial_data`。
153
+
154
+ ### 旧方法名(已废弃,仍可用)
155
+
156
+ 从仓库内旧客户端迁移过来的方法名保留为别名,方便存量代码不改:
157
+ `get_history` → `get_stock_history`、`get_market_sector` → `get_sector_data`、
158
+ `get_company` → `get_company_profile`、`get_last_tick` → `get_realtime`、
159
+ `get_market_fear_greed` → `get_fear_greed`。
160
+ 注意**出错语义已变**:旧版是「打印后返回 None」,现在是抛 `DataBullError`。
161
+
162
+ ## 版本
163
+
164
+ 版本号唯一源是 `databull/client.py` 的 `__version__`:
165
+
166
+ ```python
167
+ import databull
168
+ print(databull.__version__)
169
+ ```
170
+
171
+ ## 版权
172
+
173
+ Copyright (c) 2025 yccheni@163.com. All rights reserved.
@@ -0,0 +1,9 @@
1
+ databull/__init__.py,sha256=KNKY8qrMLKshuYel5ALMV4aT2jcgxj_Zylt_84NQO7s,654
2
+ databull/__main__.py,sha256=8fA_BIfqseLEprmZXNQlABdO4a83qJDkvr5AYLmMwyQ,402
3
+ databull/client.py,sha256=jkojNHO2nHZFoF5jngEBEhKQmTGGFguBsCX-iQysf2w,33257
4
+ databull/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ databull-0.1.0.dist-info/METADATA,sha256=4cYhxxDE9RqRKOYLyCIB7qwjpZNXjxzrKvqw-Tqxnv4,8105
6
+ databull-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ databull-0.1.0.dist-info/entry_points.txt,sha256=qdC5DqrA1Q_B2CPL770K__CrUfARex3jcuEhEqYkqfs,50
8
+ databull-0.1.0.dist-info/top_level.txt,sha256=nmV0Xq_4UQq6kbG_O5B3LZ-cwXJNXjJXpKDK2XBv9lY,9
9
+ databull-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ databull = databull.client:_cli
@@ -0,0 +1 @@
1
+ databull