lib-x17-fintech 2.1.3__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.
- lib_x17_fintech-2.1.3.dist-info/METADATA +633 -0
- lib_x17_fintech-2.1.3.dist-info/RECORD +282 -0
- lib_x17_fintech-2.1.3.dist-info/WHEEL +5 -0
- lib_x17_fintech-2.1.3.dist-info/licenses/LICENSE +1 -0
- lib_x17_fintech-2.1.3.dist-info/top_level.txt +1 -0
- xfintech/__init__.py +0 -0
- xfintech/connect/__init__.py +18 -0
- xfintech/connect/artifact/__init__.py +5 -0
- xfintech/connect/artifact/artifact.py +168 -0
- xfintech/connect/artifact/tests/__init__.py +3 -0
- xfintech/connect/artifact/tests/test_class_artifact_all.py +564 -0
- xfintech/connect/common/__init__.py +12 -0
- xfintech/connect/common/connect.py +49 -0
- xfintech/connect/common/connectref.py +119 -0
- xfintech/connect/common/error.py +62 -0
- xfintech/connect/common/tests/__init__.py +1 -0
- xfintech/connect/common/tests/test_class_connectlike_all.py +544 -0
- xfintech/connect/common/tests/test_class_connectref_all.py +586 -0
- xfintech/connect/common/tests/test_class_errors_all.py +524 -0
- xfintech/connect/instance/__init__.py +7 -0
- xfintech/connect/instance/macos.py +121 -0
- xfintech/connect/instance/s3.py +176 -0
- xfintech/connect/instance/tests/__init__.py +1 -0
- xfintech/connect/instance/tests/test_class_macosconnect_all.py +692 -0
- xfintech/connect/instance/tests/test_class_s3connect_all.py +603 -0
- xfintech/data/__init__.py +20 -0
- xfintech/data/common/__init__.py +15 -0
- xfintech/data/common/cache.py +186 -0
- xfintech/data/common/coolant.py +171 -0
- xfintech/data/common/metric.py +138 -0
- xfintech/data/common/paginate.py +132 -0
- xfintech/data/common/params.py +162 -0
- xfintech/data/common/retry.py +201 -0
- xfintech/data/common/tests/__init__.py +1 -0
- xfintech/data/common/tests/test_class_cache_all.py +681 -0
- xfintech/data/common/tests/test_class_coolant_all.py +534 -0
- xfintech/data/common/tests/test_class_metric_all.py +705 -0
- xfintech/data/common/tests/test_class_paginate_all.py +508 -0
- xfintech/data/common/tests/test_class_params_all.py +891 -0
- xfintech/data/common/tests/test_class_retry_all.py +714 -0
- xfintech/data/job/__init__.py +17 -0
- xfintech/data/job/errors.py +112 -0
- xfintech/data/job/house.py +156 -0
- xfintech/data/job/job.py +247 -0
- xfintech/data/job/joblike.py +47 -0
- xfintech/data/job/tests/__init__.py +1 -0
- xfintech/data/job/tests/test_class_errors_all.py +275 -0
- xfintech/data/job/tests/test_class_house_all.py +801 -0
- xfintech/data/job/tests/test_class_job_all.py +684 -0
- xfintech/data/job/tests/test_class_joblike_all.py +482 -0
- xfintech/data/relay/__init__.py +7 -0
- xfintech/data/relay/client.py +114 -0
- xfintech/data/relay/clientlike.py +45 -0
- xfintech/data/relay/tests/test_class_relayclient_all.py +484 -0
- xfintech/data/relay/tests/test_class_relayclientlike_all.py +500 -0
- xfintech/data/source/__init__.py +7 -0
- xfintech/data/source/baostock/__init__.py +21 -0
- xfintech/data/source/baostock/job/__init__.py +5 -0
- xfintech/data/source/baostock/job/job.py +217 -0
- xfintech/data/source/baostock/job/tests/__init__.py +0 -0
- xfintech/data/source/baostock/job/tests/test_class_baostockjob_all.py +547 -0
- xfintech/data/source/baostock/session/__init__.py +8 -0
- xfintech/data/source/baostock/session/relay.py +223 -0
- xfintech/data/source/baostock/session/session.py +241 -0
- xfintech/data/source/baostock/session/tests/__init__.py +0 -0
- xfintech/data/source/baostock/session/tests/test_class_relay_all.py +694 -0
- xfintech/data/source/baostock/session/tests/test_class_session_all.py +505 -0
- xfintech/data/source/baostock/stock/__init__.py +0 -0
- xfintech/data/source/baostock/stock/hs300stock/__init__.py +3 -0
- xfintech/data/source/baostock/stock/hs300stock/constant.py +49 -0
- xfintech/data/source/baostock/stock/hs300stock/hs300stock.py +133 -0
- xfintech/data/source/baostock/stock/hs300stock/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/hs300stock/tests/test_class_hs300index_all.py +413 -0
- xfintech/data/source/baostock/stock/minuteline/__init__.py +19 -0
- xfintech/data/source/baostock/stock/minuteline/constant.py +89 -0
- xfintech/data/source/baostock/stock/minuteline/minuteline.py +163 -0
- xfintech/data/source/baostock/stock/minuteline/tests/__init__.py +0 -0
- xfintech/data/source/baostock/stock/minuteline/tests/test_class_minuteline_all.py +582 -0
- xfintech/data/source/baostock/stock/stock/__init__.py +19 -0
- xfintech/data/source/baostock/stock/stock/constant.py +55 -0
- xfintech/data/source/baostock/stock/stock/stock.py +149 -0
- xfintech/data/source/baostock/stock/stock/tests/__init__.py +0 -0
- xfintech/data/source/baostock/stock/stock/tests/test_class_stock_all.py +508 -0
- xfintech/data/source/baostock/stock/stockinfo/__init__.py +5 -0
- xfintech/data/source/baostock/stock/stockinfo/constant.py +66 -0
- xfintech/data/source/baostock/stock/stockinfo/stockinfo.py +176 -0
- xfintech/data/source/baostock/stock/stockinfo/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/stockinfo/tests/test_class_stockinfo_all.py +617 -0
- xfintech/data/source/baostock/stock/sz50stock/__init__.py +3 -0
- xfintech/data/source/baostock/stock/sz50stock/constant.py +49 -0
- xfintech/data/source/baostock/stock/sz50stock/sz50stock.py +133 -0
- xfintech/data/source/baostock/stock/sz50stock/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/sz50stock/tests/test_class_sz50stock_all.py +397 -0
- xfintech/data/source/baostock/stock/tradedate/__init__.py +19 -0
- xfintech/data/source/baostock/stock/tradedate/constant.py +72 -0
- xfintech/data/source/baostock/stock/tradedate/tests/__init__.py +0 -0
- xfintech/data/source/baostock/stock/tradedate/tests/test_class_tradedate_all.py +695 -0
- xfintech/data/source/baostock/stock/tradedate/tradedate.py +208 -0
- xfintech/data/source/baostock/stock/zz500stock/__init__.py +3 -0
- xfintech/data/source/baostock/stock/zz500stock/constant.py +55 -0
- xfintech/data/source/baostock/stock/zz500stock/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/zz500stock/tests/test_class_zz500stock_all.py +421 -0
- xfintech/data/source/baostock/stock/zz500stock/zz500stock.py +133 -0
- xfintech/data/source/tushare/__init__.py +61 -0
- xfintech/data/source/tushare/job/__init__.py +5 -0
- xfintech/data/source/tushare/job/job.py +257 -0
- xfintech/data/source/tushare/job/tests/test_class_tusharejob_all.py +589 -0
- xfintech/data/source/tushare/session/__init__.py +5 -0
- xfintech/data/source/tushare/session/relay.py +231 -0
- xfintech/data/source/tushare/session/session.py +239 -0
- xfintech/data/source/tushare/session/tests/test_class_relay_all.py +719 -0
- xfintech/data/source/tushare/session/tests/test_class_session_all.py +705 -0
- xfintech/data/source/tushare/stock/__init__.py +55 -0
- xfintech/data/source/tushare/stock/adjfactor/__init__.py +19 -0
- xfintech/data/source/tushare/stock/adjfactor/adjfactor.py +150 -0
- xfintech/data/source/tushare/stock/adjfactor/constant.py +71 -0
- xfintech/data/source/tushare/stock/adjfactor/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/adjfactor/tests/test_class_adjfactor_all.py +372 -0
- xfintech/data/source/tushare/stock/capflow/__init__.py +19 -0
- xfintech/data/source/tushare/stock/capflow/capflow.py +171 -0
- xfintech/data/source/tushare/stock/capflow/constant.py +105 -0
- xfintech/data/source/tushare/stock/capflow/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/capflow/tests/test_class_capflow_all.py +589 -0
- xfintech/data/source/tushare/stock/capflowdc/__init__.py +19 -0
- xfintech/data/source/tushare/stock/capflowdc/capflowdc.py +167 -0
- xfintech/data/source/tushare/stock/capflowdc/constant.py +95 -0
- xfintech/data/source/tushare/stock/capflowdc/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/capflowdc/tests/test_class_capflowdc_all.py +814 -0
- xfintech/data/source/tushare/stock/capflowths/__init__.py +19 -0
- xfintech/data/source/tushare/stock/capflowths/capflowths.py +173 -0
- xfintech/data/source/tushare/stock/capflowths/constant.py +92 -0
- xfintech/data/source/tushare/stock/capflowths/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/capflowths/tests/test_class_capflowths_all.py +551 -0
- xfintech/data/source/tushare/stock/company/__init__.py +19 -0
- xfintech/data/source/tushare/stock/company/company.py +188 -0
- xfintech/data/source/tushare/stock/company/constant.py +92 -0
- xfintech/data/source/tushare/stock/company/tests/__init__.py +1 -0
- xfintech/data/source/tushare/stock/company/tests/test_class_company_all.py +829 -0
- xfintech/data/source/tushare/stock/companybusiness/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companybusiness/companybusiness.py +183 -0
- xfintech/data/source/tushare/stock/companybusiness/constant.py +91 -0
- xfintech/data/source/tushare/stock/companybusiness/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companybusiness/tests/test_class_companybusiness_all.py +633 -0
- xfintech/data/source/tushare/stock/companycashflow/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companycashflow/companycashflow.py +277 -0
- xfintech/data/source/tushare/stock/companycashflow/constant.py +293 -0
- xfintech/data/source/tushare/stock/companycashflow/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companycashflow/tests/test_class_companycashflow_all.py +619 -0
- xfintech/data/source/tushare/stock/companydebt/__init__.py +19 -0
- xfintech/data/source/tushare/stock/companydebt/companydebt.py +339 -0
- xfintech/data/source/tushare/stock/companydebt/constant.py +403 -0
- xfintech/data/source/tushare/stock/companydebt/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companydebt/tests/test_class_companydebt_all.py +655 -0
- xfintech/data/source/tushare/stock/companyoverview/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companyoverview/companyoverview.py +214 -0
- xfintech/data/source/tushare/stock/companyoverview/constant.py +152 -0
- xfintech/data/source/tushare/stock/companyoverview/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companyoverview/tests/test_class_companyoverview_all.py +647 -0
- xfintech/data/source/tushare/stock/companyprofit/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companyprofit/companyprofit.py +272 -0
- xfintech/data/source/tushare/stock/companyprofit/constant.py +259 -0
- xfintech/data/source/tushare/stock/companyprofit/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companyprofit/tests/test_class_companyprofit_all.py +635 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/__init__.py +21 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/conceptcapflowdc.py +175 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/constant.py +106 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/tests/test_class_conceptcapflowdc_all.py +568 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/__init__.py +21 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/conceptcapflowths.py +188 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/constant.py +89 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/tests/test_class_conceptcapflowths_all.py +516 -0
- xfintech/data/source/tushare/stock/dayline/__init__.py +19 -0
- xfintech/data/source/tushare/stock/dayline/constant.py +87 -0
- xfintech/data/source/tushare/stock/dayline/dayline.py +177 -0
- xfintech/data/source/tushare/stock/dayline/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/dayline/tests/test_class_dayline_all.py +585 -0
- xfintech/data/source/tushare/stock/industrycapflowths/__init__.py +21 -0
- xfintech/data/source/tushare/stock/industrycapflowths/constant.py +89 -0
- xfintech/data/source/tushare/stock/industrycapflowths/industrycapflowths.py +192 -0
- xfintech/data/source/tushare/stock/industrycapflowths/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/industrycapflowths/tests/test_class_industrycapflowths_all.py +683 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/__init__.py +21 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/constant.py +90 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/marketindexcapflowdc.py +173 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/tests/test_class_marketindexcapflowdc_all.py +793 -0
- xfintech/data/source/tushare/stock/monthline/__init__.py +19 -0
- xfintech/data/source/tushare/stock/monthline/constant.py +87 -0
- xfintech/data/source/tushare/stock/monthline/monthline.py +180 -0
- xfintech/data/source/tushare/stock/monthline/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/monthline/tests/test_class_monthline_all.py +574 -0
- xfintech/data/source/tushare/stock/stock/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stock/constant.py +105 -0
- xfintech/data/source/tushare/stock/stock/stock.py +193 -0
- xfintech/data/source/tushare/stock/stock/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stock/tests/test_class_stock_all.py +788 -0
- xfintech/data/source/tushare/stock/stockdividend/__init__.py +21 -0
- xfintech/data/source/tushare/stock/stockdividend/constant.py +111 -0
- xfintech/data/source/tushare/stock/stockdividend/stockdividend.py +180 -0
- xfintech/data/source/tushare/stock/stockdividend/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockdividend/tests/test_class_stockdividend_all.py +725 -0
- xfintech/data/source/tushare/stock/stockinfo/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockinfo/constant.py +104 -0
- xfintech/data/source/tushare/stock/stockinfo/stockinfo.py +208 -0
- xfintech/data/source/tushare/stock/stockinfo/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockinfo/tests/test_class_stockinfo_all.py +881 -0
- xfintech/data/source/tushare/stock/stockipo/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockipo/constant.py +90 -0
- xfintech/data/source/tushare/stock/stockipo/stockipo.py +234 -0
- xfintech/data/source/tushare/stock/stockipo/tests/__init__.py +1 -0
- xfintech/data/source/tushare/stock/stockipo/tests/test_class_stockipo_all.py +750 -0
- xfintech/data/source/tushare/stock/stockpledge/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockpledge/constant.py +72 -0
- xfintech/data/source/tushare/stock/stockpledge/stockpledge.py +158 -0
- xfintech/data/source/tushare/stock/stockpledge/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockpledge/tests/test_class_stockpledge_all.py +664 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/__init__.py +21 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/constant.py +85 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/stockpledgedetail.py +171 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/tests/test_class_stockpledgedetail_all.py +112 -0
- xfintech/data/source/tushare/stock/stockst/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockst/constant.py +80 -0
- xfintech/data/source/tushare/stock/stockst/stockst.py +189 -0
- xfintech/data/source/tushare/stock/stockst/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockst/tests/test_class_stockst_all.py +693 -0
- xfintech/data/source/tushare/stock/stocksuspend/__init__.py +21 -0
- xfintech/data/source/tushare/stock/stocksuspend/constant.py +75 -0
- xfintech/data/source/tushare/stock/stocksuspend/stocksuspend.py +151 -0
- xfintech/data/source/tushare/stock/stocksuspend/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stocksuspend/tests/test_class_stocksuspend_all.py +626 -0
- xfintech/data/source/tushare/stock/techindex/__init__.py +19 -0
- xfintech/data/source/tushare/stock/techindex/constant.py +600 -0
- xfintech/data/source/tushare/stock/techindex/techindex.py +314 -0
- xfintech/data/source/tushare/stock/techindex/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/techindex/tests/test_class_techindex_all.py +576 -0
- xfintech/data/source/tushare/stock/tradedate/__init__.py +19 -0
- xfintech/data/source/tushare/stock/tradedate/constant.py +93 -0
- xfintech/data/source/tushare/stock/tradedate/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/tradedate/tests/test_class_tradedate_all.py +947 -0
- xfintech/data/source/tushare/stock/tradedate/tradedate.py +234 -0
- xfintech/data/source/tushare/stock/weekline/__init__.py +19 -0
- xfintech/data/source/tushare/stock/weekline/constant.py +87 -0
- xfintech/data/source/tushare/stock/weekline/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/weekline/tests/test_class_weekline_all.py +575 -0
- xfintech/data/source/tushare/stock/weekline/weekline.py +182 -0
- xfintech/fabric/__init__.py +18 -0
- xfintech/fabric/column/__init__.py +7 -0
- xfintech/fabric/column/info.py +202 -0
- xfintech/fabric/column/kind.py +102 -0
- xfintech/fabric/column/tests/__init__.py +0 -0
- xfintech/fabric/column/tests/test_class_info_all.py +207 -0
- xfintech/fabric/column/tests/test_class_kind_all.py +80 -0
- xfintech/fabric/table/__init__.py +5 -0
- xfintech/fabric/table/info.py +263 -0
- xfintech/fabric/table/tests/__init__.py +0 -0
- xfintech/fabric/table/tests/test_class_info_all.py +547 -0
- xfintech/serde/__init__.py +35 -0
- xfintech/serde/common/__init__.py +9 -0
- xfintech/serde/common/dataformat.py +78 -0
- xfintech/serde/common/deserialiserlike.py +38 -0
- xfintech/serde/common/error.py +182 -0
- xfintech/serde/common/serialiserlike.py +38 -0
- xfintech/serde/common/tests/__init__.py +1 -0
- xfintech/serde/common/tests/test_class_dataformat_all.py +694 -0
- xfintech/serde/common/tests/test_class_deserialiserlike_all.py +500 -0
- xfintech/serde/common/tests/test_class_errors_all.py +518 -0
- xfintech/serde/common/tests/test_class_serialiserlike_all.py +401 -0
- xfintech/serde/deserialiser/__init__.py +7 -0
- xfintech/serde/deserialiser/pandas.py +113 -0
- xfintech/serde/deserialiser/python.py +68 -0
- xfintech/serde/deserialiser/tests/__init__.py +1 -0
- xfintech/serde/deserialiser/tests/test_class_pandasdeserialiser_all.py +503 -0
- xfintech/serde/deserialiser/tests/test_class_pythondeserialiser_all.py +570 -0
- xfintech/serde/serialiser/__init__.py +7 -0
- xfintech/serde/serialiser/pandas.py +116 -0
- xfintech/serde/serialiser/python.py +71 -0
- xfintech/serde/serialiser/tests/__init__.py +1 -0
- xfintech/serde/serialiser/tests/test_class_pandasserialiser_all.py +474 -0
- xfintech/serde/serialiser/tests/test_class_pythonserialiser_all.py +508 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for Stock class
|
|
3
|
+
Tests cover initialization, data fetching, transformation, and utility methods
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from xfintech.data.common.cache import Cache
|
|
12
|
+
from xfintech.data.common.coolant import Coolant
|
|
13
|
+
from xfintech.data.common.paginate import Paginate
|
|
14
|
+
from xfintech.data.common.params import Params
|
|
15
|
+
from xfintech.data.common.retry import Retry
|
|
16
|
+
from xfintech.data.source.tushare.session.session import Session
|
|
17
|
+
from xfintech.data.source.tushare.stock.stock.constant import (
|
|
18
|
+
EXCHANGES,
|
|
19
|
+
KEY,
|
|
20
|
+
NAME,
|
|
21
|
+
PAGINATE,
|
|
22
|
+
SOURCE,
|
|
23
|
+
STATUSES,
|
|
24
|
+
TARGET,
|
|
25
|
+
)
|
|
26
|
+
from xfintech.data.source.tushare.stock.stock.stock import Stock
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# Fixtures
|
|
30
|
+
# ============================================================================
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def mock_session():
|
|
35
|
+
"""Create a mock Tushare session"""
|
|
36
|
+
session = MagicMock(spec=Session)
|
|
37
|
+
session._credential = "test_token"
|
|
38
|
+
session.id = "test1234"
|
|
39
|
+
session.mode = "direct"
|
|
40
|
+
session.relay_url = None
|
|
41
|
+
session.relay_secret = None
|
|
42
|
+
session.connected = True
|
|
43
|
+
|
|
44
|
+
# Mock the connection object (which is returned by ts.pro_api())
|
|
45
|
+
mock_connection = MagicMock()
|
|
46
|
+
mock_connection.stock_basic = MagicMock()
|
|
47
|
+
session.connection = mock_connection
|
|
48
|
+
|
|
49
|
+
return session
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def sample_source_data():
|
|
54
|
+
"""Create sample source data in Tushare format"""
|
|
55
|
+
return pd.DataFrame(
|
|
56
|
+
{
|
|
57
|
+
"ts_code": ["000001.SZ", "000002.SZ", "600000.SH"],
|
|
58
|
+
"symbol": ["000001", "000002", "600000"],
|
|
59
|
+
"name": ["平安银行", "万科A", "浦发银行"],
|
|
60
|
+
"area": ["深圳", "深圳", "上海"],
|
|
61
|
+
"industry": ["银行", "房地产", "银行"],
|
|
62
|
+
"fullname": ["平安银行股份有限公司", "万科企业股份有限公司", "上海浦东发展银行股份有限公司"],
|
|
63
|
+
"enname": ["Ping An Bank", "China Vanke", "Shanghai Pudong Development Bank"],
|
|
64
|
+
"cnspell": ["PAYH", "WKA", "PFFH"],
|
|
65
|
+
"market": ["主板", "主板", "主板"],
|
|
66
|
+
"exchange": ["SZSE", "SZSE", "SSE"],
|
|
67
|
+
"curr_type": ["CNY", "CNY", "CNY"],
|
|
68
|
+
"list_status": ["L", "L", "L"],
|
|
69
|
+
"list_date": ["19910403", "19910129", "19991110"],
|
|
70
|
+
"delist_date": ["", "", ""],
|
|
71
|
+
"is_hs": ["S", "S", "S"],
|
|
72
|
+
"act_name": ["平安集团", "华润", "上海国资"],
|
|
73
|
+
"act_ent_type": ["央企", "央企", "地方国资"],
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def expected_transformed_data():
|
|
80
|
+
"""Create expected transformed data"""
|
|
81
|
+
return pd.DataFrame(
|
|
82
|
+
{
|
|
83
|
+
"code": ["000001.SZ", "000002.SZ", "600000.SH"],
|
|
84
|
+
"symbol": ["000001", "000002", "600000"],
|
|
85
|
+
"name": ["平安银行", "万科A", "浦发银行"],
|
|
86
|
+
"area": ["深圳", "深圳", "上海"],
|
|
87
|
+
"industry": ["银行", "房地产", "银行"],
|
|
88
|
+
"fullname": ["平安银行股份有限公司", "万科企业股份有限公司", "上海浦东发展银行股份有限公司"],
|
|
89
|
+
"enname": ["Ping An Bank", "China Vanke", "Shanghai Pudong Development Bank"],
|
|
90
|
+
"cnspell": ["PAYH", "WKA", "PFFH"],
|
|
91
|
+
"market": ["主板", "主板", "主板"],
|
|
92
|
+
"exchange": ["SZSE", "SZSE", "SSE"],
|
|
93
|
+
"currency": ["CNY", "CNY", "CNY"],
|
|
94
|
+
"list_status": ["L", "L", "L"],
|
|
95
|
+
"list_date": ["1991-04-03", "1991-01-29", "1999-11-10"],
|
|
96
|
+
"delist_date": ["NaT", "NaT", "NaT"],
|
|
97
|
+
"is_hs": ["S", "S", "S"],
|
|
98
|
+
"ace_name": ["平安集团", "华润", "上海国资"],
|
|
99
|
+
"ace_type": ["央企", "央企", "地方国资"],
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================================
|
|
105
|
+
# Initialization Tests
|
|
106
|
+
# ============================================================================
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_stock_init_basic(mock_session):
|
|
110
|
+
"""Test Stock initialization with minimal parameters"""
|
|
111
|
+
stock = Stock(session=mock_session)
|
|
112
|
+
|
|
113
|
+
assert stock.name == NAME
|
|
114
|
+
assert stock.key == KEY
|
|
115
|
+
assert stock.source == SOURCE
|
|
116
|
+
assert stock.target == TARGET
|
|
117
|
+
assert isinstance(stock.params, Params)
|
|
118
|
+
assert isinstance(stock.coolant, Coolant)
|
|
119
|
+
assert isinstance(stock.paginate, Paginate)
|
|
120
|
+
assert isinstance(stock.retry, Retry)
|
|
121
|
+
assert stock.paginate.pagesize == PAGINATE.pagesize
|
|
122
|
+
assert stock.paginate.pagelimit == PAGINATE.pagelimit
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_stock_init_with_params_dict(mock_session):
|
|
126
|
+
"""Test Stock initialization with params as dict"""
|
|
127
|
+
params = {"list_status": "L", "ts_code": "600000.SH"}
|
|
128
|
+
stock = Stock(session=mock_session, params=params)
|
|
129
|
+
|
|
130
|
+
assert stock.params.list_status == "L"
|
|
131
|
+
assert stock.params.ts_code == "600000.SH"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_stock_init_with_params_object(mock_session):
|
|
135
|
+
"""Test Stock initialization with Params object"""
|
|
136
|
+
params = Params(list_status="D", ts_code="000001.SZ")
|
|
137
|
+
stock = Stock(session=mock_session, params=params)
|
|
138
|
+
|
|
139
|
+
assert stock.params.list_status == "D"
|
|
140
|
+
assert stock.params.ts_code == "000001.SZ"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_stock_init_with_cache_bool_true(mock_session):
|
|
144
|
+
"""Test Stock initialization with cache=True"""
|
|
145
|
+
stock = Stock(session=mock_session, cache=True)
|
|
146
|
+
|
|
147
|
+
assert stock.cache is not None
|
|
148
|
+
assert isinstance(stock.cache, Cache)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_stock_init_with_cache_bool_false(mock_session):
|
|
152
|
+
"""Test Stock initialization with cache=False"""
|
|
153
|
+
stock = Stock(session=mock_session, cache=False)
|
|
154
|
+
|
|
155
|
+
assert stock.cache is None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_stock_init_with_cache_dict(mock_session):
|
|
159
|
+
"""Test Stock initialization with cache as dict"""
|
|
160
|
+
cache_config = {"directory": "/tmp/cache"}
|
|
161
|
+
stock = Stock(session=mock_session, cache=cache_config)
|
|
162
|
+
|
|
163
|
+
assert stock.cache is not None
|
|
164
|
+
assert isinstance(stock.cache, Cache)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_stock_init_with_all_params(mock_session):
|
|
168
|
+
"""Test Stock initialization with all parameters"""
|
|
169
|
+
stock = Stock(
|
|
170
|
+
session=mock_session,
|
|
171
|
+
params={"list_status": "L"},
|
|
172
|
+
coolant={"interval": 1.0},
|
|
173
|
+
retry={"max_retries": 3},
|
|
174
|
+
cache=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
assert stock.name == NAME
|
|
178
|
+
assert stock.params.list_status == "L"
|
|
179
|
+
assert stock.cache is not None
|
|
180
|
+
assert stock.paginate.pagesize == PAGINATE.pagesize
|
|
181
|
+
assert stock.paginate.pagelimit == PAGINATE.pagelimit
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_stock_constants():
|
|
185
|
+
"""Test that constants are properly defined"""
|
|
186
|
+
assert NAME == "stock"
|
|
187
|
+
assert KEY == "/tushare/stock"
|
|
188
|
+
assert EXCHANGES == ["SSE", "SZSE", "BSE"]
|
|
189
|
+
assert STATUSES == ["L", "D", "P"]
|
|
190
|
+
assert SOURCE is not None
|
|
191
|
+
assert TARGET is not None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ============================================================================
|
|
195
|
+
# Transform Method Tests
|
|
196
|
+
# ============================================================================
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_stock_transform_basic(mock_session, sample_source_data):
|
|
200
|
+
"""Test basic data transformation"""
|
|
201
|
+
stock = Stock(session=mock_session)
|
|
202
|
+
result = stock.transform(sample_source_data)
|
|
203
|
+
|
|
204
|
+
assert not result.empty
|
|
205
|
+
assert len(result) == 3
|
|
206
|
+
assert "code" in result.columns
|
|
207
|
+
assert "name" in result.columns
|
|
208
|
+
assert "list_date" in result.columns
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_stock_transform_code_mapping(mock_session, sample_source_data):
|
|
212
|
+
"""Test that ts_code is mapped to code"""
|
|
213
|
+
stock = Stock(session=mock_session)
|
|
214
|
+
result = stock.transform(sample_source_data)
|
|
215
|
+
|
|
216
|
+
assert result["code"].tolist() == ["000001.SZ", "000002.SZ", "600000.SH"]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_stock_transform_name_mapping(mock_session, sample_source_data):
|
|
220
|
+
"""Test that name is preserved"""
|
|
221
|
+
stock = Stock(session=mock_session)
|
|
222
|
+
result = stock.transform(sample_source_data)
|
|
223
|
+
|
|
224
|
+
assert "平安银行" in result["name"].values
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_stock_transform_date_format(mock_session, sample_source_data):
|
|
228
|
+
"""Test that list_date is converted from YYYYMMDD to YYYY-MM-DD"""
|
|
229
|
+
stock = Stock(session=mock_session)
|
|
230
|
+
result = stock.transform(sample_source_data)
|
|
231
|
+
|
|
232
|
+
assert result["list_date"].tolist() == ["1991-04-03", "1991-01-29", "1999-11-10"]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_stock_transform_currency_mapping(mock_session, sample_source_data):
|
|
236
|
+
"""Test that curr_type is mapped to currency"""
|
|
237
|
+
stock = Stock(session=mock_session)
|
|
238
|
+
result = stock.transform(sample_source_data)
|
|
239
|
+
|
|
240
|
+
assert "currency" in result.columns
|
|
241
|
+
assert all(result["currency"] == "CNY")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_stock_transform_ace_name_mapping(mock_session, sample_source_data):
|
|
245
|
+
"""Test that act_name is mapped to ace_name"""
|
|
246
|
+
stock = Stock(session=mock_session)
|
|
247
|
+
result = stock.transform(sample_source_data)
|
|
248
|
+
|
|
249
|
+
assert "ace_name" in result.columns
|
|
250
|
+
assert "平安集团" in result["ace_name"].values
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_stock_transform_empty_dataframe(mock_session):
|
|
254
|
+
"""Test transform with empty DataFrame"""
|
|
255
|
+
stock = Stock(session=mock_session)
|
|
256
|
+
empty_df = pd.DataFrame()
|
|
257
|
+
result = stock.transform(empty_df)
|
|
258
|
+
|
|
259
|
+
assert result.empty
|
|
260
|
+
assert list(result.columns) == TARGET.list_column_names()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_stock_transform_none_input(mock_session):
|
|
264
|
+
"""Test transform with None input"""
|
|
265
|
+
stock = Stock(session=mock_session)
|
|
266
|
+
result = stock.transform(None)
|
|
267
|
+
|
|
268
|
+
assert result.empty
|
|
269
|
+
assert list(result.columns) == TARGET.list_column_names()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_stock_transform_handles_invalid_dates(mock_session):
|
|
273
|
+
"""Test transform handles invalid date formats"""
|
|
274
|
+
stock = Stock(session=mock_session)
|
|
275
|
+
data = pd.DataFrame(
|
|
276
|
+
{
|
|
277
|
+
"ts_code": ["000001.SZ"],
|
|
278
|
+
"symbol": ["000001"],
|
|
279
|
+
"name": ["Test Stock"],
|
|
280
|
+
"area": ["Test"],
|
|
281
|
+
"industry": ["Test"],
|
|
282
|
+
"fullname": ["Test Company"],
|
|
283
|
+
"enname": ["Test"],
|
|
284
|
+
"cnspell": ["TS"],
|
|
285
|
+
"market": ["主板"],
|
|
286
|
+
"exchange": ["SZSE"],
|
|
287
|
+
"curr_type": ["CNY"],
|
|
288
|
+
"list_status": ["L"],
|
|
289
|
+
"list_date": ["invalid"], # Invalid date
|
|
290
|
+
"delist_date": [""],
|
|
291
|
+
"is_hs": ["N"],
|
|
292
|
+
"act_name": ["Test"],
|
|
293
|
+
"act_ent_type": ["Test"],
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
result = stock.transform(data)
|
|
298
|
+
# Should handle error with coerce
|
|
299
|
+
assert pd.isna(result["list_date"].iloc[0]) or result["list_date"].iloc[0] == "NaT"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_stock_transform_removes_duplicates(mock_session):
|
|
303
|
+
"""Test that transform removes duplicate rows"""
|
|
304
|
+
stock = Stock(session=mock_session)
|
|
305
|
+
data = pd.DataFrame(
|
|
306
|
+
{
|
|
307
|
+
"ts_code": ["000001.SZ", "000001.SZ"], # Duplicate
|
|
308
|
+
"symbol": ["000001", "000001"],
|
|
309
|
+
"name": ["Test", "Test"],
|
|
310
|
+
"area": ["Test", "Test"],
|
|
311
|
+
"industry": ["Test", "Test"],
|
|
312
|
+
"fullname": ["Test", "Test"],
|
|
313
|
+
"enname": ["Test", "Test"],
|
|
314
|
+
"cnspell": ["TS", "TS"],
|
|
315
|
+
"market": ["主板", "主板"],
|
|
316
|
+
"exchange": ["SZSE", "SZSE"],
|
|
317
|
+
"curr_type": ["CNY", "CNY"],
|
|
318
|
+
"list_status": ["L", "L"],
|
|
319
|
+
"list_date": ["20200101", "20200101"],
|
|
320
|
+
"delist_date": ["", ""],
|
|
321
|
+
"is_hs": ["N", "N"],
|
|
322
|
+
"act_name": ["Test", "Test"],
|
|
323
|
+
"act_ent_type": ["Test", "Test"],
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
result = stock.transform(data)
|
|
328
|
+
assert len(result) == 1
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_stock_transform_sorts_by_code(mock_session, sample_source_data):
|
|
332
|
+
"""Test that result is sorted by code"""
|
|
333
|
+
stock = Stock(session=mock_session)
|
|
334
|
+
# Shuffle the data
|
|
335
|
+
shuffled = sample_source_data.sample(frac=1).reset_index(drop=True)
|
|
336
|
+
result = stock.transform(shuffled)
|
|
337
|
+
|
|
338
|
+
# Should be sorted
|
|
339
|
+
assert result["code"].tolist() == sorted(result["code"].tolist())
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_stock_transform_resets_index(mock_session, sample_source_data):
|
|
343
|
+
"""Test that result has reset index"""
|
|
344
|
+
stock = Stock(session=mock_session)
|
|
345
|
+
result = stock.transform(sample_source_data)
|
|
346
|
+
|
|
347
|
+
assert result.index.tolist() == list(range(len(result)))
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_stock_transform_only_target_columns(mock_session, sample_source_data):
|
|
351
|
+
"""Test that only target columns are in result"""
|
|
352
|
+
stock = Stock(session=mock_session)
|
|
353
|
+
result = stock.transform(sample_source_data)
|
|
354
|
+
|
|
355
|
+
expected_cols = set(TARGET.list_column_names())
|
|
356
|
+
actual_cols = set(result.columns)
|
|
357
|
+
assert actual_cols == expected_cols
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ============================================================================
|
|
361
|
+
# _run Method Tests
|
|
362
|
+
# ============================================================================
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_stock_run_with_cache_hit(mock_session):
|
|
366
|
+
"""Test _run returns cached data when available"""
|
|
367
|
+
stock = Stock(session=mock_session, cache=True)
|
|
368
|
+
|
|
369
|
+
# Set up cached data
|
|
370
|
+
cached_df = pd.DataFrame({"code": ["000001.SZ"]})
|
|
371
|
+
stock.cache.set(stock.params.identifier, cached_df)
|
|
372
|
+
|
|
373
|
+
result = stock._run()
|
|
374
|
+
|
|
375
|
+
# Should return cached data without calling API
|
|
376
|
+
assert result.equals(cached_df)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_stock_run_without_list_status_param(mock_session, sample_source_data):
|
|
380
|
+
"""Test _run queries all statuses when list_status not specified"""
|
|
381
|
+
stock = Stock(session=mock_session)
|
|
382
|
+
|
|
383
|
+
# Mock _fetchall to return sample data
|
|
384
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
385
|
+
with patch.object(stock, "transform", return_value=sample_source_data):
|
|
386
|
+
stock._run()
|
|
387
|
+
|
|
388
|
+
# Should call _fetchall for each status
|
|
389
|
+
assert stock._fetchall.call_count == len(STATUSES)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_stock_run_with_list_status_param(mock_session, sample_source_data):
|
|
393
|
+
"""Test _run queries specific status when provided"""
|
|
394
|
+
stock = Stock(session=mock_session, params={"list_status": "L"})
|
|
395
|
+
|
|
396
|
+
# Mock _fetchall
|
|
397
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
398
|
+
with patch.object(stock, "transform", return_value=sample_source_data):
|
|
399
|
+
stock._run()
|
|
400
|
+
|
|
401
|
+
# Should call _fetchall only once
|
|
402
|
+
assert stock._fetchall.call_count == 1
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def test_stock_run_adds_fields_param(mock_session, sample_source_data):
|
|
406
|
+
"""Test _run adds fields parameter if not provided"""
|
|
407
|
+
stock = Stock(session=mock_session)
|
|
408
|
+
|
|
409
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
410
|
+
with patch.object(stock, "transform", return_value=sample_source_data):
|
|
411
|
+
stock._run()
|
|
412
|
+
|
|
413
|
+
# Check that fields were added to params
|
|
414
|
+
call_args = stock._fetchall.call_args
|
|
415
|
+
assert "fields" in call_args[1]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def test_stock_run_preserves_fields_param(mock_session, sample_source_data):
|
|
419
|
+
"""Test _run preserves existing fields parameter"""
|
|
420
|
+
custom_fields = "ts_code,name,symbol"
|
|
421
|
+
stock = Stock(session=mock_session, params={"fields": custom_fields})
|
|
422
|
+
|
|
423
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
424
|
+
with patch.object(stock, "transform", return_value=sample_source_data):
|
|
425
|
+
stock._run()
|
|
426
|
+
|
|
427
|
+
# Should use provided fields
|
|
428
|
+
assert stock.params.fields == custom_fields
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_stock_run_sets_cache(mock_session, sample_source_data):
|
|
432
|
+
"""Test _run saves result to cache"""
|
|
433
|
+
stock = Stock(session=mock_session, cache=True)
|
|
434
|
+
|
|
435
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
436
|
+
with patch.object(stock, "transform", return_value=sample_source_data):
|
|
437
|
+
stock._run()
|
|
438
|
+
|
|
439
|
+
# Check cache was set
|
|
440
|
+
cached = stock.cache.get(stock.params.identifier)
|
|
441
|
+
assert cached is not None
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_stock_run_calls_transform(mock_session, sample_source_data):
|
|
445
|
+
"""Test _run calls transform method"""
|
|
446
|
+
stock = Stock(session=mock_session)
|
|
447
|
+
|
|
448
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
449
|
+
with patch.object(stock, "transform", return_value=sample_source_data) as mock_transform:
|
|
450
|
+
stock._run()
|
|
451
|
+
|
|
452
|
+
# Transform should be called
|
|
453
|
+
assert mock_transform.called
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def test_stock_run_concatenates_multiple_statuses(mock_session, sample_source_data):
|
|
457
|
+
"""Test _run concatenates data from multiple statuses"""
|
|
458
|
+
stock = Stock(session=mock_session)
|
|
459
|
+
|
|
460
|
+
# Create different data for each status
|
|
461
|
+
listed_data = sample_source_data[sample_source_data["list_status"] == "L"]
|
|
462
|
+
delisted_data = sample_source_data.copy()
|
|
463
|
+
delisted_data["list_status"] = "D"
|
|
464
|
+
|
|
465
|
+
call_count = [0]
|
|
466
|
+
|
|
467
|
+
def mock_fetchall(*args, **kwargs):
|
|
468
|
+
call_count[0] += 1
|
|
469
|
+
if call_count[0] == 1:
|
|
470
|
+
return listed_data
|
|
471
|
+
elif call_count[0] == 2:
|
|
472
|
+
return delisted_data
|
|
473
|
+
else:
|
|
474
|
+
return pd.DataFrame()
|
|
475
|
+
|
|
476
|
+
with patch.object(stock, "_fetchall", side_effect=mock_fetchall):
|
|
477
|
+
with patch.object(stock, "transform", side_effect=lambda x: x):
|
|
478
|
+
result = stock._run()
|
|
479
|
+
|
|
480
|
+
# Should have data from multiple statuses
|
|
481
|
+
assert len(result) >= 1
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ============================================================================
|
|
485
|
+
# list_codes Method Tests
|
|
486
|
+
# ============================================================================
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_stock_list_codes_basic(mock_session, sample_source_data):
|
|
490
|
+
"""Test list_codes returns list of stock codes"""
|
|
491
|
+
stock = Stock(session=mock_session, cache=True)
|
|
492
|
+
|
|
493
|
+
# Mock the run to return sample data
|
|
494
|
+
transformed = stock.transform(sample_source_data)
|
|
495
|
+
stock.cache.set(stock.params.identifier, transformed)
|
|
496
|
+
|
|
497
|
+
codes = stock.list_codes()
|
|
498
|
+
|
|
499
|
+
assert isinstance(codes, list)
|
|
500
|
+
assert len(codes) == 3
|
|
501
|
+
assert "000001.SZ" in codes
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_stock_list_codes_unique(mock_session):
|
|
505
|
+
"""Test list_codes returns unique codes"""
|
|
506
|
+
stock = Stock(session=mock_session, cache=True)
|
|
507
|
+
|
|
508
|
+
# Create data with duplicates
|
|
509
|
+
df = pd.DataFrame(
|
|
510
|
+
{
|
|
511
|
+
"code": ["000001.SZ", "000001.SZ", "000002.SZ"],
|
|
512
|
+
}
|
|
513
|
+
)
|
|
514
|
+
stock.cache.set(stock.params.identifier, df)
|
|
515
|
+
|
|
516
|
+
codes = stock.list_codes()
|
|
517
|
+
|
|
518
|
+
assert len(codes) == 2 # Only unique codes
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_stock_list_codes_calls_run_when_not_cached(mock_session, sample_source_data):
|
|
522
|
+
"""Test list_codes calls run() when data not in cache"""
|
|
523
|
+
stock = Stock(session=mock_session)
|
|
524
|
+
|
|
525
|
+
with patch.object(stock, "run", return_value=stock.transform(sample_source_data)):
|
|
526
|
+
codes = stock.list_codes()
|
|
527
|
+
|
|
528
|
+
# run should have been called
|
|
529
|
+
stock.run.assert_called_once()
|
|
530
|
+
assert len(codes) == 3
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_stock_list_codes_uses_cache(mock_session, sample_source_data):
|
|
534
|
+
"""Test list_codes uses cached data when available"""
|
|
535
|
+
stock = Stock(session=mock_session, cache=True)
|
|
536
|
+
|
|
537
|
+
transformed = stock.transform(sample_source_data)
|
|
538
|
+
stock.cache.set(stock.params.identifier, transformed)
|
|
539
|
+
|
|
540
|
+
# Mock _fetchall to verify it's not called when cache exists
|
|
541
|
+
with patch.object(stock, "_fetchall") as mock_fetch:
|
|
542
|
+
codes = stock.list_codes()
|
|
543
|
+
|
|
544
|
+
# _fetchall should NOT be called when cache exists
|
|
545
|
+
mock_fetch.assert_not_called()
|
|
546
|
+
assert len(codes) == 3
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ============================================================================
|
|
550
|
+
# list_names Method Tests
|
|
551
|
+
# ============================================================================
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def test_stock_list_names_basic(mock_session, sample_source_data):
|
|
555
|
+
"""Test list_names returns list of stock names"""
|
|
556
|
+
stock = Stock(session=mock_session, cache=True)
|
|
557
|
+
|
|
558
|
+
transformed = stock.transform(sample_source_data)
|
|
559
|
+
stock.cache.set(stock.params.identifier, transformed)
|
|
560
|
+
|
|
561
|
+
names = stock.list_names()
|
|
562
|
+
|
|
563
|
+
assert isinstance(names, list)
|
|
564
|
+
assert len(names) == 3
|
|
565
|
+
assert "平安银行" in names
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def test_stock_list_names_sorted(mock_session, sample_source_data):
|
|
569
|
+
"""Test list_names returns sorted list"""
|
|
570
|
+
stock = Stock(session=mock_session, cache=True)
|
|
571
|
+
|
|
572
|
+
transformed = stock.transform(sample_source_data)
|
|
573
|
+
stock.cache.set(stock.params.identifier, transformed)
|
|
574
|
+
|
|
575
|
+
names = stock.list_names()
|
|
576
|
+
|
|
577
|
+
assert names == sorted(names)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def test_stock_list_names_unique(mock_session):
|
|
581
|
+
"""Test list_names returns unique names"""
|
|
582
|
+
stock = Stock(session=mock_session, cache=True)
|
|
583
|
+
|
|
584
|
+
df = pd.DataFrame(
|
|
585
|
+
{
|
|
586
|
+
"name": ["平安银行", "平安银行", "万科A"],
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
stock.cache.set(stock.params.identifier, df)
|
|
590
|
+
|
|
591
|
+
names = stock.list_names()
|
|
592
|
+
|
|
593
|
+
assert len(names) == 2 # Only unique names
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def test_stock_list_names_calls_run_when_not_cached(mock_session, sample_source_data):
|
|
597
|
+
"""Test list_names calls run() when data not in cache"""
|
|
598
|
+
stock = Stock(session=mock_session)
|
|
599
|
+
|
|
600
|
+
with patch.object(stock, "run", return_value=stock.transform(sample_source_data)):
|
|
601
|
+
names = stock.list_names()
|
|
602
|
+
|
|
603
|
+
stock.run.assert_called_once()
|
|
604
|
+
assert len(names) == 3
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def test_stock_list_names_uses_cache(mock_session, sample_source_data):
|
|
608
|
+
"""Test list_names uses cached data when available"""
|
|
609
|
+
stock = Stock(session=mock_session, cache=True)
|
|
610
|
+
|
|
611
|
+
transformed = stock.transform(sample_source_data)
|
|
612
|
+
stock.cache.set(stock.params.identifier, transformed)
|
|
613
|
+
|
|
614
|
+
# Mock _fetchall to verify it's not called when cache exists
|
|
615
|
+
with patch.object(stock, "_fetchall") as mock_fetch:
|
|
616
|
+
names = stock.list_names()
|
|
617
|
+
|
|
618
|
+
# _fetchall should NOT be called when cache exists
|
|
619
|
+
mock_fetch.assert_not_called()
|
|
620
|
+
assert len(names) == 3
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ============================================================================
|
|
624
|
+
# Integration Tests
|
|
625
|
+
# ============================================================================
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def test_stock_full_workflow(mock_session, sample_source_data):
|
|
629
|
+
"""Test complete workflow from initialization to data retrieval"""
|
|
630
|
+
stock = Stock(
|
|
631
|
+
session=mock_session,
|
|
632
|
+
params={"list_status": "L"},
|
|
633
|
+
cache=True,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
637
|
+
# Run the job
|
|
638
|
+
result = stock.run()
|
|
639
|
+
|
|
640
|
+
assert not result.empty
|
|
641
|
+
assert "code" in result.columns
|
|
642
|
+
|
|
643
|
+
# Get codes and names
|
|
644
|
+
codes = stock.list_codes()
|
|
645
|
+
names = stock.list_names()
|
|
646
|
+
|
|
647
|
+
assert len(codes) > 0
|
|
648
|
+
assert len(names) > 0
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def test_stock_multiple_statuses_integration(mock_session, sample_source_data):
|
|
652
|
+
"""Test fetching data from multiple statuses"""
|
|
653
|
+
stock = Stock(session=mock_session, cache=True)
|
|
654
|
+
|
|
655
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
656
|
+
result = stock.run()
|
|
657
|
+
|
|
658
|
+
# Should have data from multiple statuses
|
|
659
|
+
unique_statuses = result["list_status"].unique()
|
|
660
|
+
assert len(unique_statuses) >= 1
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def test_stock_cache_persistence(mock_session, sample_source_data):
|
|
664
|
+
"""Test that cache persists across method calls"""
|
|
665
|
+
stock = Stock(session=mock_session, cache=True)
|
|
666
|
+
|
|
667
|
+
with patch.object(stock, "_load_cache", return_value=None) as mock_load:
|
|
668
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data) as mock_fetch:
|
|
669
|
+
# First call - fetches data and caches it
|
|
670
|
+
result1 = stock.run()
|
|
671
|
+
assert mock_fetch.call_count == len(STATUSES) # Once per status
|
|
672
|
+
assert mock_load.call_count == 1
|
|
673
|
+
|
|
674
|
+
# Second call - _load_cache still returns None, so _fetchall called again
|
|
675
|
+
result2 = stock.run()
|
|
676
|
+
assert mock_fetch.call_count == len(STATUSES) * 2 # Called again for each status
|
|
677
|
+
assert mock_load.call_count == 2
|
|
678
|
+
|
|
679
|
+
pd.testing.assert_frame_equal(result1, result2)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def test_stock_params_identifier_uniqueness(mock_session):
|
|
683
|
+
"""Test that different params produce different cache keys"""
|
|
684
|
+
stock1 = Stock(session=mock_session, params={"list_status": "L"}, cache=True)
|
|
685
|
+
stock2 = Stock(session=mock_session, params={"list_status": "D"}, cache=True)
|
|
686
|
+
|
|
687
|
+
assert stock1.params.identifier != stock2.params.identifier
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# ============================================================================
|
|
691
|
+
# Edge Case Tests
|
|
692
|
+
# ============================================================================
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def test_stock_empty_result_handling(mock_session):
|
|
696
|
+
"""Test handling of empty API results"""
|
|
697
|
+
stock = Stock(session=mock_session)
|
|
698
|
+
|
|
699
|
+
empty_df = pd.DataFrame()
|
|
700
|
+
with patch.object(stock, "_fetchall", return_value=empty_df):
|
|
701
|
+
result = stock._run()
|
|
702
|
+
|
|
703
|
+
assert result.empty
|
|
704
|
+
assert list(result.columns) == TARGET.list_column_names()
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_stock_large_dataset_handling(mock_session):
|
|
708
|
+
"""Test handling of large datasets"""
|
|
709
|
+
stock = Stock(session=mock_session)
|
|
710
|
+
|
|
711
|
+
# Create large dataset
|
|
712
|
+
large_data = pd.DataFrame(
|
|
713
|
+
{
|
|
714
|
+
"ts_code": [f"{i:06d}.SZ" for i in range(5000)],
|
|
715
|
+
"symbol": [f"{i:06d}" for i in range(5000)],
|
|
716
|
+
"name": [f"Stock {i}" for i in range(5000)],
|
|
717
|
+
"area": ["Test"] * 5000,
|
|
718
|
+
"industry": ["Test"] * 5000,
|
|
719
|
+
"fullname": [f"Company {i}" for i in range(5000)],
|
|
720
|
+
"enname": ["Test"] * 5000,
|
|
721
|
+
"cnspell": ["TS"] * 5000,
|
|
722
|
+
"market": ["主板"] * 5000,
|
|
723
|
+
"exchange": ["SZSE"] * 5000,
|
|
724
|
+
"curr_type": ["CNY"] * 5000,
|
|
725
|
+
"list_status": ["L"] * 5000,
|
|
726
|
+
"list_date": ["20200101"] * 5000,
|
|
727
|
+
"delist_date": [""] * 5000,
|
|
728
|
+
"is_hs": ["N"] * 5000,
|
|
729
|
+
"act_name": ["Test"] * 5000,
|
|
730
|
+
"act_ent_type": ["Test"] * 5000,
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
result = stock.transform(large_data)
|
|
735
|
+
|
|
736
|
+
assert len(result) == 5000
|
|
737
|
+
assert not result.empty
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def test_stock_special_characters_in_data(mock_session):
|
|
741
|
+
"""Test handling of special characters in stock data"""
|
|
742
|
+
stock = Stock(session=mock_session)
|
|
743
|
+
|
|
744
|
+
data = pd.DataFrame(
|
|
745
|
+
{
|
|
746
|
+
"ts_code": ["000001.SZ"],
|
|
747
|
+
"symbol": ["000001"],
|
|
748
|
+
"name": ["股票名称(中文)& Special <Chars>"],
|
|
749
|
+
"area": ["深圳"],
|
|
750
|
+
"industry": ["Test & Industry"],
|
|
751
|
+
"fullname": ["公司全称 & info"],
|
|
752
|
+
"enname": ["Company Name <Special>"],
|
|
753
|
+
"cnspell": ["GPMZ"],
|
|
754
|
+
"market": ["主板"],
|
|
755
|
+
"exchange": ["SZSE"],
|
|
756
|
+
"curr_type": ["CNY"],
|
|
757
|
+
"list_status": ["L"],
|
|
758
|
+
"list_date": ["20200101"],
|
|
759
|
+
"delist_date": [""],
|
|
760
|
+
"is_hs": ["N"],
|
|
761
|
+
"act_name": ["实控人@公司"],
|
|
762
|
+
"act_ent_type": ["央企"],
|
|
763
|
+
}
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
result = stock.transform(data)
|
|
767
|
+
|
|
768
|
+
assert len(result) == 1
|
|
769
|
+
assert "特" in result["name"].values[0] or "股" in result["name"].values[0]
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def test_stock_without_cache(mock_session, sample_source_data):
|
|
773
|
+
"""Test Stock works correctly without cache"""
|
|
774
|
+
stock = Stock(session=mock_session, cache=False)
|
|
775
|
+
|
|
776
|
+
assert stock.cache is None
|
|
777
|
+
|
|
778
|
+
with patch.object(stock, "_fetchall", return_value=sample_source_data):
|
|
779
|
+
result = stock.run()
|
|
780
|
+
|
|
781
|
+
assert not result.empty
|
|
782
|
+
|
|
783
|
+
# list_codes and list_names should still work
|
|
784
|
+
codes = stock.list_codes()
|
|
785
|
+
names = stock.list_names()
|
|
786
|
+
|
|
787
|
+
assert len(codes) > 0
|
|
788
|
+
assert len(names) > 0
|