From 23e43f70d6c0befd29a469f5ed85900533641566 Mon Sep 17 00:00:00 2001 From: Paris Qian <1759658+qiansen1386@users.noreply.github.com> Date: Mon, 16 Dec 2024 03:19:18 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20pandas=201.0.0?= =?UTF-8?q?+=20=E7=9A=84=E6=96=B0=20json=5Fnormalize=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lixinger_openapi/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lixinger_openapi/query.py b/lixinger_openapi/query.py index e8e2e3f..5795dff 100644 --- a/lixinger_openapi/query.py +++ b/lixinger_openapi/query.py @@ -4,7 +4,7 @@ ''' import json import requests -from pandas.io.json import json_normalize +from pandas import json_normalize from lixinger_openapi.token import get_token BASEURL = "https://open.lixinger.com/api/" From b8fcdd4fe35bf44da223317f5cd7e0ebf9de0f7d Mon Sep 17 00:00:00 2001 From: Paris Qian <1759658+qiansen1386@users.noreply.github.com> Date: Tue, 17 Dec 2024 02:12:57 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20python3=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E6=8A=A5=E9=94=99=EF=BC=9Arequests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirement.txt | 3 +++ setup.py | 24 +++++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 requirement.txt diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000..f93122d --- /dev/null +++ b/requirement.txt @@ -0,0 +1,3 @@ +pandas +requests +tox diff --git a/setup.py b/setup.py index 0ff442f..ea0d93c 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,15 @@ -# -*- coding: utf-8 -*- ''' 理杏仁开放平台API包安装 ''' + import lixinger_openapi as lo -from setuptools import ( - find_packages, - setup, -) +from setuptools import find_packages, setup +# 获取版本号 version = lo.__version__ -with open("README.md", "r") as fh: +# 读取README.md内容作为长描述 +with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup( @@ -24,13 +23,16 @@ author_email='lvxueji@gmail.com', license='Apache License v2', install_requires=[ - "requests", - "pandas", + "requests>=1.0.0", + "pandas>=1.0.0", ], url='https://github.com/ShekiLyu/lixinger-openapi', classifiers=[ 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3', # 移除对Python 2.7的支持 + 'License :: OSI Approved :: Apache Software License', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries :: Python Modules', ], -) + python_requires='>=3.6', # 指定最低Python版本要求 +) \ No newline at end of file From 01559de0de69238eca22e384d8cbd653f35142eb Mon Sep 17 00:00:00 2001 From: Paris Qian <1759658+qiansen1386@users.noreply.github.com> Date: Tue, 17 Dec 2024 02:30:55 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=97=AE=E9=A2=98=EF=BC=8C=E5=9C=A8=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=97=B6=E7=A7=BB=E9=99=A4=E5=AF=B9requests=E7=9A=84?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lixinger_openapi/__init__.py | 9 ++++---- lixinger_openapi/_version.py | 1 + requirement.txt | 3 ++- setup.py | 44 +++++++++++++++++++++--------------- tox.ini | 10 ++++++++ 5 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 lixinger_openapi/_version.py create mode 100644 tox.ini diff --git a/lixinger_openapi/__init__.py b/lixinger_openapi/__init__.py index 201a9a8..e104274 100644 --- a/lixinger_openapi/__init__.py +++ b/lixinger_openapi/__init__.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- -''' +""" 理杏仁开放平台API,非官方 -''' +""" from lixinger_openapi.query import query_json, query_dataframe from lixinger_openapi.token import set_token +from ._version import __version__ -__version__ = '1.0.2' - -__author__ = 'sheki lyu' +__author__ = "sheki lyu" name = "lixinger_openapi" diff --git a/lixinger_openapi/_version.py b/lixinger_openapi/_version.py new file mode 100644 index 0000000..77139f6 --- /dev/null +++ b/lixinger_openapi/_version.py @@ -0,0 +1 @@ +__version__ = '1.0.3' \ No newline at end of file diff --git a/requirement.txt b/requirement.txt index f93122d..2ff8486 100644 --- a/requirement.txt +++ b/requirement.txt @@ -1,3 +1,4 @@ pandas requests -tox +setuptools +tox \ No newline at end of file diff --git a/setup.py b/setup.py index ea0d93c..443321d 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,46 @@ -''' +""" 理杏仁开放平台API包安装 -''' +""" -import lixinger_openapi as lo +import os from setuptools import find_packages, setup + # 获取版本号 -version = lo.__version__ +def get_version(): + version_file = os.path.join( + os.path.dirname(__file__), "lixinger_openapi", "_version.py" + ) + with open(version_file, "r", encoding="utf-8") as f: + exec(f.read()) + return locals()["__version__"] + # 读取README.md内容作为长描述 with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup( - name='lixinger_openapi', - version=version, - description='lixinger openapi', + name="lixinger_openapi", + version=get_version(), + description="lixinger openapi", long_description=long_description, long_description_content_type="text/markdown", packages=find_packages(), - author='sheki lyu', - author_email='lvxueji@gmail.com', - license='Apache License v2', + author="sheki lyu", + author_email="lvxueji@gmail.com", + license="Apache License v2", install_requires=[ "requests>=1.0.0", "pandas>=1.0.0", ], - url='https://github.com/ShekiLyu/lixinger-openapi', + url="https://github.com/ShekiLyu/lixinger-openapi", classifiers=[ - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3', # 移除对Python 2.7的支持 - 'License :: OSI Approved :: Apache Software License', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", # 移除对Python 2.7的支持 + "License :: OSI Approved :: Apache Software License", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", ], - python_requires='>=3.6', # 指定最低Python版本要求 -) \ No newline at end of file + python_requires=">=3.6", # 指定最低Python版本要求 +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8ed8316 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py36, py37, py38, py39, py310, py311, py312, py313 + +[testenv] +deps = + pandas + requests + setuptools +commands = + pytest \ No newline at end of file From eb0f9f77d267e02c974a897f10ef27ba7fc7762b Mon Sep 17 00:00:00 2001 From: Paris Qian <1759658+qiansen1386@users.noreply.github.com> Date: Tue, 17 Dec 2024 02:49:39 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20import=20version=20=E6=96=B0?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 443321d..e129ea7 100644 --- a/setup.py +++ b/setup.py @@ -2,18 +2,28 @@ 理杏仁开放平台API包安装 """ -import os +import importlib.util +from pathlib import Path +from types import ModuleType + from setuptools import find_packages, setup -# 获取版本号 -def get_version(): - version_file = os.path.join( - os.path.dirname(__file__), "lixinger_openapi", "_version.py" - ) - with open(version_file, "r", encoding="utf-8") as f: - exec(f.read()) - return locals()["__version__"] +def get_version() -> str: + """ + 获取 _version.py 文件里的版本号 + """ + version_file: Path = Path(__file__) / "lixinger_openapi" / "_version.py" + assert version_file is not None, "版本文件不存在" + spec = importlib.util.spec_from_file_location("_version", version_file) + if spec is None or spec.loader is None: + print(f"Warning: Could not load version from {version_file}") + return "" + + # 使用 importlib.util.module_from_spec 创建一个新的模块对象,并执行 + version_module: ModuleType = importlib.util.module_from_spec(spec) + spec.loader.exec_module(version_module) + return version_module.__version__ # 读取README.md内容作为长描述 @@ -31,8 +41,9 @@ def get_version(): author_email="lvxueji@gmail.com", license="Apache License v2", install_requires=[ - "requests>=1.0.0", - "pandas>=1.0.0", + "requests", + "pandas", + "numpy", ], url="https://github.com/ShekiLyu/lixinger-openapi", classifiers=[ From 86c79d07a6e4ae8442dcc31a7414718535e61ea2 Mon Sep 17 00:00:00 2001 From: Paris Qian <1759658+qiansen1386@users.noreply.github.com> Date: Tue, 17 Dec 2024 03:01:13 +0800 Subject: [PATCH 5/7] fix: path reference --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e129ea7..a963136 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def get_version() -> str: """ 获取 _version.py 文件里的版本号 """ - version_file: Path = Path(__file__) / "lixinger_openapi" / "_version.py" + version_file: Path = Path(__file__).parent / "lixinger_openapi" / "_version.py" assert version_file is not None, "版本文件不存在" spec = importlib.util.spec_from_file_location("_version", version_file) if spec is None or spec.loader is None: From f01279f77c0d0306dd414aa4eda1ef0910ef6f71 Mon Sep 17 00:00:00 2001 From: Paris Qian <1759658+qiansen1386@users.noreply.github.com> Date: Wed, 18 Dec 2024 02:27:56 +0800 Subject: [PATCH 6/7] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=E6=89=80?= =?UTF-8?q?=E6=9C=89=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirement.txt | 4 +- test/query_test.py | 176 +++++++++++++++++++++++++++------------------ 2 files changed, 110 insertions(+), 70 deletions(-) diff --git a/requirement.txt b/requirement.txt index 2ff8486..ad1fde8 100644 --- a/requirement.txt +++ b/requirement.txt @@ -1,4 +1,6 @@ +numpy pandas requests setuptools -tox \ No newline at end of file +tox +pytest \ No newline at end of file diff --git a/test/query_test.py b/test/query_test.py index 356270a..333fa05 100644 --- a/test/query_test.py +++ b/test/query_test.py @@ -1,84 +1,122 @@ # -*- coding: utf-8 -*- -''' +""" 测试代码 注意:因为token值涉及账户隐私,不能在代码里写,所以请在代码所在目录下创建一个token.cfg文件,并将token值写入。token.cfg文件我已经在git里忽略,不会被上库。 -''' -import sys, os -if os.path.abspath('..') not in sys.path: - sys.path.append(os.path.abspath('..')) +""" + +from datetime import datetime +from pprint import pp +import sys +import os import unittest -from lixinger_openapi.token import set_token +import pandas as pd + +if os.path.abspath("..") not in sys.path: + sys.path.append(os.path.abspath("..")) from lixinger_openapi.query import ( query_json, query_dataframe, ) + class DataTest(unittest.TestCase): def setUp(self): pass - #你可以在这里运行一次set_token,写入token.cfg文件,然后再删除token值,这样有了cfg文件以后都不用set_token了。 - #set_token("") - - def test_query_json_001(self): - rlt = query_json('a.stock', { - "industryType": "bank" - }) - self.assertEqual(0, rlt['code']) - self.assertEqual("success", rlt['msg']) - self.assertEqual("000001", rlt['data'][0]['stockCode']) - - def test_query_dataframe_001(self): - rlt = query_dataframe('a.stock', { - "industryType": "bank" - }) - self.assertEqual(0, rlt['code']) - self.assertEqual("success", rlt['msg']) - self.assertEqual("000001", rlt['data'].loc[0, 'stockCode']) - - def test_query_json_002(self): - rlt = query_json('a.stock.indice', { - "stockCode": "000028" - }) - self.assertEqual(0, rlt['code']) - self.assertEqual("success", rlt['msg']) - self.assertEqual("cn", rlt['data'][0]['areaCode']) - self.assertEqual("399348", rlt['data'][0]['stockCode']) - - def test_query_json_003(self): - rlt = query_json('a.stock.industry', { - "stockCode": "000028" - }) - self.assertEqual(0, rlt['code']) - self.assertEqual("success", rlt['msg']) - self.assertEqual("cn", rlt['data'][0]['areaCode']) - self.assertEqual("C05", rlt['data'][0]['stockCode']) - - def test_query_json_004(self): - rlt = query_json('a.indice.fundamental', { - "date": "2018-01-19", - "stockCodes": ["000016"], - "metrics": [ - "pe_ttm.y_10.weightedAvg", - "pe_ttm.weightedAvg", - "mc" - ] - }) - self.assertEqual(0, rlt['code']) - self.assertEqual("success", rlt['msg']) - self.assertEqual("2018-01-19", rlt['data'][0]['date'][:10]) - self.assertLess(12, rlt['data'][0]['pe_ttm']['weightedAvg']) - - def test_query_json_005(self): - rlt = query_json('a.indice.samples', { - "date": "2017-09-30", - "stockCodes": ["000016"] - }) - self.assertEqual(0, rlt['code']) - self.assertEqual("success", rlt['msg']) - self.assertEqual("000016", rlt['data'][0]['stockCode']) - self.assertEqual("600000", rlt['data'][0]['samples'][0]) - -if __name__ == '__main__': + # 你可以在这里运行一次set_token,写入token.cfg文件,然后再删除token值,这样有了cfg文件以后都不用set_token了。 + # set_token("") + + # 大陆-公司-基础信息 + def test_query_json_company(self): + rlt = query_json("cn/company", {"fsTableType": "bank"}) + self.assertIn("code", rlt) + self.assertEqual("success", rlt["message"]) + self.assertGreater(len(rlt["data"]), 0) + self.assertIn("000001", [x["stockCode"] for x in rlt["data"]]) + + # 大陆-公司-基础信息 + def test_query_df_company(self): + rlt: dict = query_dataframe("cn/company", {"fsTableType": "bank"}) + self.assertIn("code", rlt) + self.assertGreater(len(rlt), 0) + df: pd.DataFrame = rlt["data"] + self.assertTrue(isinstance(df, pd.DataFrame)) + self.assertTrue((df["stockCode"] == "000001").any()) + + # 大陆-公司-所属指数 + def test_query_json_index(self): + """测试宁德时代在深圳成指""" + rlt = query_json("cn/company/indices", {"stockCode": "300750"}) + pp(rlt) + self.assertIn("code", rlt) + self.assertEqual("success", rlt["message"]) + self.assertGreater(len(rlt), 0) + self.assertEqual("cn", rlt["data"][0]["areaCode"]) + self.assertTrue( + any( + r + for r in rlt["data"] + if "stockCode" in r and r["stockCode"] == "399001" + ) + ) + + # 大陆-公司-股票所属行业 + def test_query_json_company_industries(self): + rlt = query_json("cn/company/industries", {"stockCode": "300750"}) + pp(rlt) + self.assertIn("code", rlt) + self.assertEqual("success", rlt["message"]) + self.assertGreater(len(rlt), 0) + self.assertEqual("cn", rlt["data"][0]["areaCode"]) + self.assertTrue( + any(r for r in rlt["data"] if "stockCode" in r and r["stockCode"] == "C03") + ) + + # 大陆-指数-基本面数据 + def test_query_json_fundamental(self): + date_to_test = "2024-12-10" + rlt = query_json( + "cn/index/fundamental", + { + "date": date_to_test, + "stockCodes": ["000016"], + "metricsList": ["pe_ttm.y10.mcw.cvpos", "pe_ttm.mcw", "mc"], + }, + ) + pp(rlt) + self.assertIn("code", rlt) + self.assertEqual("success", rlt["message"]) + self.assertGreater(len(rlt["data"]), 0) + data = rlt["data"][0] + self.assertEqual(date_to_test, _extract_date(data["date"])) + self.assertAlmostEqual(10.75, rlt["data"][0]["pe_ttm.mcw"], delta=0.01) + + # 大陆-指数-指数样本 + def test_query_json_index_samples(self): + rlt = query_json( + "cn/index/constituents", {"date": "2017-09-30", "stockCodes": ["000016"]} + ) + pp(rlt) + self.assertIn("code", rlt) + self.assertEqual("success", rlt["message"]) + self.assertIn("000016", [x["stockCode"] for x in rlt["data"]]) + + # 美股-指数-基本信息 + def test_query_df_us_index(self): + rlt = query_dataframe("us/index", {}) + self.assertIn("code", rlt) + self.assertEqual("", rlt["msg"]) + df: pd.DataFrame = rlt["data"] + assert isinstance(df, pd.DataFrame) + self.assertTrue(isinstance(df, pd.DataFrame)) + self.assertGreater(len(df), 0) + self.assertTrue((df["name"] == "标普500").any()) + + +def _extract_date(datetime_str: str) -> str: + return datetime.fromisoformat(datetime_str).strftime("%Y-%m-%d") + + +if __name__ == "__main__": unittest.main() From 749c3cea9f2e43f2f11a593a93cb639ca4288ca3 Mon Sep 17 00:00:00 2001 From: Paris Qian <1759658+qiansen1386@users.noreply.github.com> Date: Wed, 18 Dec 2024 02:34:16 +0800 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20=E8=A1=A5=E5=85=85=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/query_test.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/query_test.py b/test/query_test.py index 333fa05..ef5d17c 100644 --- a/test/query_test.py +++ b/test/query_test.py @@ -13,6 +13,7 @@ import pandas as pd +# 将上级目录添加到系统路径中,以便导入自定义模块 if os.path.abspath("..") not in sys.path: sys.path.append(os.path.abspath("..")) from lixinger_openapi.query import ( @@ -22,47 +23,51 @@ class DataTest(unittest.TestCase): + """测试理杏仁开放平台API的数据查询功能""" + def setUp(self): + """你可以在这里运行一次set_token,写入token.cfg文件,然后再删除token值,这样有了cfg文件以后都不用set_token了""" pass - # 你可以在这里运行一次set_token,写入token.cfg文件,然后再删除token值,这样有了cfg文件以后都不用set_token了。 # set_token("") - # 大陆-公司-基础信息 def test_query_json_company(self): + """测试查询大陆公司基础信息(JSON格式)""" rlt = query_json("cn/company", {"fsTableType": "bank"}) + pp(rlt) self.assertIn("code", rlt) self.assertEqual("success", rlt["message"]) self.assertGreater(len(rlt["data"]), 0) self.assertIn("000001", [x["stockCode"] for x in rlt["data"]]) - # 大陆-公司-基础信息 def test_query_df_company(self): + """测试查询大陆公司基础信息(DataFrame格式)""" rlt: dict = query_dataframe("cn/company", {"fsTableType": "bank"}) + pp(rlt) self.assertIn("code", rlt) self.assertGreater(len(rlt), 0) df: pd.DataFrame = rlt["data"] self.assertTrue(isinstance(df, pd.DataFrame)) self.assertTrue((df["stockCode"] == "000001").any()) - # 大陆-公司-所属指数 def test_query_json_index(self): - """测试宁德时代在深圳成指""" + """测试查询公司所属指数(以宁德时代为例,必在深圳成指)""" rlt = query_json("cn/company/indices", {"stockCode": "300750"}) pp(rlt) self.assertIn("code", rlt) self.assertEqual("success", rlt["message"]) self.assertGreater(len(rlt), 0) self.assertEqual("cn", rlt["data"][0]["areaCode"]) + 深圳成指 = "399001" self.assertTrue( any( r for r in rlt["data"] - if "stockCode" in r and r["stockCode"] == "399001" + if "stockCode" in r and r["stockCode"] == 深圳成指 ) ) - # 大陆-公司-股票所属行业 def test_query_json_company_industries(self): + """测试查询公司所属行业(以宁德时代为例)""" rlt = query_json("cn/company/industries", {"stockCode": "300750"}) pp(rlt) self.assertIn("code", rlt) @@ -73,8 +78,8 @@ def test_query_json_company_industries(self): any(r for r in rlt["data"] if "stockCode" in r and r["stockCode"] == "C03") ) - # 大陆-指数-基本面数据 def test_query_json_fundamental(self): + """测试查询指数基本面数据(以上证50指数为例)""" date_to_test = "2024-12-10" rlt = query_json( "cn/index/fundamental", @@ -92,8 +97,8 @@ def test_query_json_fundamental(self): self.assertEqual(date_to_test, _extract_date(data["date"])) self.assertAlmostEqual(10.75, rlt["data"][0]["pe_ttm.mcw"], delta=0.01) - # 大陆-指数-指数样本 def test_query_json_index_samples(self): + """测试查询指数样本(以上证50指数为例)""" rlt = query_json( "cn/index/constituents", {"date": "2017-09-30", "stockCodes": ["000016"]} ) @@ -102,9 +107,10 @@ def test_query_json_index_samples(self): self.assertEqual("success", rlt["message"]) self.assertIn("000016", [x["stockCode"] for x in rlt["data"]]) - # 美股-指数-基本信息 def test_query_df_us_index(self): + """测试查询美股指数基本信息""" rlt = query_dataframe("us/index", {}) + pp(rlt) self.assertIn("code", rlt) self.assertEqual("", rlt["msg"]) df: pd.DataFrame = rlt["data"] @@ -115,6 +121,7 @@ def test_query_df_us_index(self): def _extract_date(datetime_str: str) -> str: + """从ISO格式的日期时间字符串中提取日期部分""" return datetime.fromisoformat(datetime_str).strftime("%Y-%m-%d")