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.
Files changed (282) hide show
  1. lib_x17_fintech-2.1.3.dist-info/METADATA +633 -0
  2. lib_x17_fintech-2.1.3.dist-info/RECORD +282 -0
  3. lib_x17_fintech-2.1.3.dist-info/WHEEL +5 -0
  4. lib_x17_fintech-2.1.3.dist-info/licenses/LICENSE +1 -0
  5. lib_x17_fintech-2.1.3.dist-info/top_level.txt +1 -0
  6. xfintech/__init__.py +0 -0
  7. xfintech/connect/__init__.py +18 -0
  8. xfintech/connect/artifact/__init__.py +5 -0
  9. xfintech/connect/artifact/artifact.py +168 -0
  10. xfintech/connect/artifact/tests/__init__.py +3 -0
  11. xfintech/connect/artifact/tests/test_class_artifact_all.py +564 -0
  12. xfintech/connect/common/__init__.py +12 -0
  13. xfintech/connect/common/connect.py +49 -0
  14. xfintech/connect/common/connectref.py +119 -0
  15. xfintech/connect/common/error.py +62 -0
  16. xfintech/connect/common/tests/__init__.py +1 -0
  17. xfintech/connect/common/tests/test_class_connectlike_all.py +544 -0
  18. xfintech/connect/common/tests/test_class_connectref_all.py +586 -0
  19. xfintech/connect/common/tests/test_class_errors_all.py +524 -0
  20. xfintech/connect/instance/__init__.py +7 -0
  21. xfintech/connect/instance/macos.py +121 -0
  22. xfintech/connect/instance/s3.py +176 -0
  23. xfintech/connect/instance/tests/__init__.py +1 -0
  24. xfintech/connect/instance/tests/test_class_macosconnect_all.py +692 -0
  25. xfintech/connect/instance/tests/test_class_s3connect_all.py +603 -0
  26. xfintech/data/__init__.py +20 -0
  27. xfintech/data/common/__init__.py +15 -0
  28. xfintech/data/common/cache.py +186 -0
  29. xfintech/data/common/coolant.py +171 -0
  30. xfintech/data/common/metric.py +138 -0
  31. xfintech/data/common/paginate.py +132 -0
  32. xfintech/data/common/params.py +162 -0
  33. xfintech/data/common/retry.py +201 -0
  34. xfintech/data/common/tests/__init__.py +1 -0
  35. xfintech/data/common/tests/test_class_cache_all.py +681 -0
  36. xfintech/data/common/tests/test_class_coolant_all.py +534 -0
  37. xfintech/data/common/tests/test_class_metric_all.py +705 -0
  38. xfintech/data/common/tests/test_class_paginate_all.py +508 -0
  39. xfintech/data/common/tests/test_class_params_all.py +891 -0
  40. xfintech/data/common/tests/test_class_retry_all.py +714 -0
  41. xfintech/data/job/__init__.py +17 -0
  42. xfintech/data/job/errors.py +112 -0
  43. xfintech/data/job/house.py +156 -0
  44. xfintech/data/job/job.py +247 -0
  45. xfintech/data/job/joblike.py +47 -0
  46. xfintech/data/job/tests/__init__.py +1 -0
  47. xfintech/data/job/tests/test_class_errors_all.py +275 -0
  48. xfintech/data/job/tests/test_class_house_all.py +801 -0
  49. xfintech/data/job/tests/test_class_job_all.py +684 -0
  50. xfintech/data/job/tests/test_class_joblike_all.py +482 -0
  51. xfintech/data/relay/__init__.py +7 -0
  52. xfintech/data/relay/client.py +114 -0
  53. xfintech/data/relay/clientlike.py +45 -0
  54. xfintech/data/relay/tests/test_class_relayclient_all.py +484 -0
  55. xfintech/data/relay/tests/test_class_relayclientlike_all.py +500 -0
  56. xfintech/data/source/__init__.py +7 -0
  57. xfintech/data/source/baostock/__init__.py +21 -0
  58. xfintech/data/source/baostock/job/__init__.py +5 -0
  59. xfintech/data/source/baostock/job/job.py +217 -0
  60. xfintech/data/source/baostock/job/tests/__init__.py +0 -0
  61. xfintech/data/source/baostock/job/tests/test_class_baostockjob_all.py +547 -0
  62. xfintech/data/source/baostock/session/__init__.py +8 -0
  63. xfintech/data/source/baostock/session/relay.py +223 -0
  64. xfintech/data/source/baostock/session/session.py +241 -0
  65. xfintech/data/source/baostock/session/tests/__init__.py +0 -0
  66. xfintech/data/source/baostock/session/tests/test_class_relay_all.py +694 -0
  67. xfintech/data/source/baostock/session/tests/test_class_session_all.py +505 -0
  68. xfintech/data/source/baostock/stock/__init__.py +0 -0
  69. xfintech/data/source/baostock/stock/hs300stock/__init__.py +3 -0
  70. xfintech/data/source/baostock/stock/hs300stock/constant.py +49 -0
  71. xfintech/data/source/baostock/stock/hs300stock/hs300stock.py +133 -0
  72. xfintech/data/source/baostock/stock/hs300stock/tests/__init__.py +1 -0
  73. xfintech/data/source/baostock/stock/hs300stock/tests/test_class_hs300index_all.py +413 -0
  74. xfintech/data/source/baostock/stock/minuteline/__init__.py +19 -0
  75. xfintech/data/source/baostock/stock/minuteline/constant.py +89 -0
  76. xfintech/data/source/baostock/stock/minuteline/minuteline.py +163 -0
  77. xfintech/data/source/baostock/stock/minuteline/tests/__init__.py +0 -0
  78. xfintech/data/source/baostock/stock/minuteline/tests/test_class_minuteline_all.py +582 -0
  79. xfintech/data/source/baostock/stock/stock/__init__.py +19 -0
  80. xfintech/data/source/baostock/stock/stock/constant.py +55 -0
  81. xfintech/data/source/baostock/stock/stock/stock.py +149 -0
  82. xfintech/data/source/baostock/stock/stock/tests/__init__.py +0 -0
  83. xfintech/data/source/baostock/stock/stock/tests/test_class_stock_all.py +508 -0
  84. xfintech/data/source/baostock/stock/stockinfo/__init__.py +5 -0
  85. xfintech/data/source/baostock/stock/stockinfo/constant.py +66 -0
  86. xfintech/data/source/baostock/stock/stockinfo/stockinfo.py +176 -0
  87. xfintech/data/source/baostock/stock/stockinfo/tests/__init__.py +1 -0
  88. xfintech/data/source/baostock/stock/stockinfo/tests/test_class_stockinfo_all.py +617 -0
  89. xfintech/data/source/baostock/stock/sz50stock/__init__.py +3 -0
  90. xfintech/data/source/baostock/stock/sz50stock/constant.py +49 -0
  91. xfintech/data/source/baostock/stock/sz50stock/sz50stock.py +133 -0
  92. xfintech/data/source/baostock/stock/sz50stock/tests/__init__.py +1 -0
  93. xfintech/data/source/baostock/stock/sz50stock/tests/test_class_sz50stock_all.py +397 -0
  94. xfintech/data/source/baostock/stock/tradedate/__init__.py +19 -0
  95. xfintech/data/source/baostock/stock/tradedate/constant.py +72 -0
  96. xfintech/data/source/baostock/stock/tradedate/tests/__init__.py +0 -0
  97. xfintech/data/source/baostock/stock/tradedate/tests/test_class_tradedate_all.py +695 -0
  98. xfintech/data/source/baostock/stock/tradedate/tradedate.py +208 -0
  99. xfintech/data/source/baostock/stock/zz500stock/__init__.py +3 -0
  100. xfintech/data/source/baostock/stock/zz500stock/constant.py +55 -0
  101. xfintech/data/source/baostock/stock/zz500stock/tests/__init__.py +1 -0
  102. xfintech/data/source/baostock/stock/zz500stock/tests/test_class_zz500stock_all.py +421 -0
  103. xfintech/data/source/baostock/stock/zz500stock/zz500stock.py +133 -0
  104. xfintech/data/source/tushare/__init__.py +61 -0
  105. xfintech/data/source/tushare/job/__init__.py +5 -0
  106. xfintech/data/source/tushare/job/job.py +257 -0
  107. xfintech/data/source/tushare/job/tests/test_class_tusharejob_all.py +589 -0
  108. xfintech/data/source/tushare/session/__init__.py +5 -0
  109. xfintech/data/source/tushare/session/relay.py +231 -0
  110. xfintech/data/source/tushare/session/session.py +239 -0
  111. xfintech/data/source/tushare/session/tests/test_class_relay_all.py +719 -0
  112. xfintech/data/source/tushare/session/tests/test_class_session_all.py +705 -0
  113. xfintech/data/source/tushare/stock/__init__.py +55 -0
  114. xfintech/data/source/tushare/stock/adjfactor/__init__.py +19 -0
  115. xfintech/data/source/tushare/stock/adjfactor/adjfactor.py +150 -0
  116. xfintech/data/source/tushare/stock/adjfactor/constant.py +71 -0
  117. xfintech/data/source/tushare/stock/adjfactor/tests/__init__.py +0 -0
  118. xfintech/data/source/tushare/stock/adjfactor/tests/test_class_adjfactor_all.py +372 -0
  119. xfintech/data/source/tushare/stock/capflow/__init__.py +19 -0
  120. xfintech/data/source/tushare/stock/capflow/capflow.py +171 -0
  121. xfintech/data/source/tushare/stock/capflow/constant.py +105 -0
  122. xfintech/data/source/tushare/stock/capflow/tests/__init__.py +0 -0
  123. xfintech/data/source/tushare/stock/capflow/tests/test_class_capflow_all.py +589 -0
  124. xfintech/data/source/tushare/stock/capflowdc/__init__.py +19 -0
  125. xfintech/data/source/tushare/stock/capflowdc/capflowdc.py +167 -0
  126. xfintech/data/source/tushare/stock/capflowdc/constant.py +95 -0
  127. xfintech/data/source/tushare/stock/capflowdc/tests/__init__.py +0 -0
  128. xfintech/data/source/tushare/stock/capflowdc/tests/test_class_capflowdc_all.py +814 -0
  129. xfintech/data/source/tushare/stock/capflowths/__init__.py +19 -0
  130. xfintech/data/source/tushare/stock/capflowths/capflowths.py +173 -0
  131. xfintech/data/source/tushare/stock/capflowths/constant.py +92 -0
  132. xfintech/data/source/tushare/stock/capflowths/tests/__init__.py +0 -0
  133. xfintech/data/source/tushare/stock/capflowths/tests/test_class_capflowths_all.py +551 -0
  134. xfintech/data/source/tushare/stock/company/__init__.py +19 -0
  135. xfintech/data/source/tushare/stock/company/company.py +188 -0
  136. xfintech/data/source/tushare/stock/company/constant.py +92 -0
  137. xfintech/data/source/tushare/stock/company/tests/__init__.py +1 -0
  138. xfintech/data/source/tushare/stock/company/tests/test_class_company_all.py +829 -0
  139. xfintech/data/source/tushare/stock/companybusiness/__init__.py +21 -0
  140. xfintech/data/source/tushare/stock/companybusiness/companybusiness.py +183 -0
  141. xfintech/data/source/tushare/stock/companybusiness/constant.py +91 -0
  142. xfintech/data/source/tushare/stock/companybusiness/tests/__init__.py +0 -0
  143. xfintech/data/source/tushare/stock/companybusiness/tests/test_class_companybusiness_all.py +633 -0
  144. xfintech/data/source/tushare/stock/companycashflow/__init__.py +21 -0
  145. xfintech/data/source/tushare/stock/companycashflow/companycashflow.py +277 -0
  146. xfintech/data/source/tushare/stock/companycashflow/constant.py +293 -0
  147. xfintech/data/source/tushare/stock/companycashflow/tests/__init__.py +0 -0
  148. xfintech/data/source/tushare/stock/companycashflow/tests/test_class_companycashflow_all.py +619 -0
  149. xfintech/data/source/tushare/stock/companydebt/__init__.py +19 -0
  150. xfintech/data/source/tushare/stock/companydebt/companydebt.py +339 -0
  151. xfintech/data/source/tushare/stock/companydebt/constant.py +403 -0
  152. xfintech/data/source/tushare/stock/companydebt/tests/__init__.py +0 -0
  153. xfintech/data/source/tushare/stock/companydebt/tests/test_class_companydebt_all.py +655 -0
  154. xfintech/data/source/tushare/stock/companyoverview/__init__.py +21 -0
  155. xfintech/data/source/tushare/stock/companyoverview/companyoverview.py +214 -0
  156. xfintech/data/source/tushare/stock/companyoverview/constant.py +152 -0
  157. xfintech/data/source/tushare/stock/companyoverview/tests/__init__.py +0 -0
  158. xfintech/data/source/tushare/stock/companyoverview/tests/test_class_companyoverview_all.py +647 -0
  159. xfintech/data/source/tushare/stock/companyprofit/__init__.py +21 -0
  160. xfintech/data/source/tushare/stock/companyprofit/companyprofit.py +272 -0
  161. xfintech/data/source/tushare/stock/companyprofit/constant.py +259 -0
  162. xfintech/data/source/tushare/stock/companyprofit/tests/__init__.py +0 -0
  163. xfintech/data/source/tushare/stock/companyprofit/tests/test_class_companyprofit_all.py +635 -0
  164. xfintech/data/source/tushare/stock/conceptcapflowdc/__init__.py +21 -0
  165. xfintech/data/source/tushare/stock/conceptcapflowdc/conceptcapflowdc.py +175 -0
  166. xfintech/data/source/tushare/stock/conceptcapflowdc/constant.py +106 -0
  167. xfintech/data/source/tushare/stock/conceptcapflowdc/tests/__init__.py +0 -0
  168. xfintech/data/source/tushare/stock/conceptcapflowdc/tests/test_class_conceptcapflowdc_all.py +568 -0
  169. xfintech/data/source/tushare/stock/conceptcapflowths/__init__.py +21 -0
  170. xfintech/data/source/tushare/stock/conceptcapflowths/conceptcapflowths.py +188 -0
  171. xfintech/data/source/tushare/stock/conceptcapflowths/constant.py +89 -0
  172. xfintech/data/source/tushare/stock/conceptcapflowths/tests/__init__.py +0 -0
  173. xfintech/data/source/tushare/stock/conceptcapflowths/tests/test_class_conceptcapflowths_all.py +516 -0
  174. xfintech/data/source/tushare/stock/dayline/__init__.py +19 -0
  175. xfintech/data/source/tushare/stock/dayline/constant.py +87 -0
  176. xfintech/data/source/tushare/stock/dayline/dayline.py +177 -0
  177. xfintech/data/source/tushare/stock/dayline/tests/__init__.py +0 -0
  178. xfintech/data/source/tushare/stock/dayline/tests/test_class_dayline_all.py +585 -0
  179. xfintech/data/source/tushare/stock/industrycapflowths/__init__.py +21 -0
  180. xfintech/data/source/tushare/stock/industrycapflowths/constant.py +89 -0
  181. xfintech/data/source/tushare/stock/industrycapflowths/industrycapflowths.py +192 -0
  182. xfintech/data/source/tushare/stock/industrycapflowths/tests/__init__.py +0 -0
  183. xfintech/data/source/tushare/stock/industrycapflowths/tests/test_class_industrycapflowths_all.py +683 -0
  184. xfintech/data/source/tushare/stock/marketindexcapflowdc/__init__.py +21 -0
  185. xfintech/data/source/tushare/stock/marketindexcapflowdc/constant.py +90 -0
  186. xfintech/data/source/tushare/stock/marketindexcapflowdc/marketindexcapflowdc.py +173 -0
  187. xfintech/data/source/tushare/stock/marketindexcapflowdc/tests/__init__.py +0 -0
  188. xfintech/data/source/tushare/stock/marketindexcapflowdc/tests/test_class_marketindexcapflowdc_all.py +793 -0
  189. xfintech/data/source/tushare/stock/monthline/__init__.py +19 -0
  190. xfintech/data/source/tushare/stock/monthline/constant.py +87 -0
  191. xfintech/data/source/tushare/stock/monthline/monthline.py +180 -0
  192. xfintech/data/source/tushare/stock/monthline/tests/__init__.py +0 -0
  193. xfintech/data/source/tushare/stock/monthline/tests/test_class_monthline_all.py +574 -0
  194. xfintech/data/source/tushare/stock/stock/__init__.py +19 -0
  195. xfintech/data/source/tushare/stock/stock/constant.py +105 -0
  196. xfintech/data/source/tushare/stock/stock/stock.py +193 -0
  197. xfintech/data/source/tushare/stock/stock/tests/__init__.py +0 -0
  198. xfintech/data/source/tushare/stock/stock/tests/test_class_stock_all.py +788 -0
  199. xfintech/data/source/tushare/stock/stockdividend/__init__.py +21 -0
  200. xfintech/data/source/tushare/stock/stockdividend/constant.py +111 -0
  201. xfintech/data/source/tushare/stock/stockdividend/stockdividend.py +180 -0
  202. xfintech/data/source/tushare/stock/stockdividend/tests/__init__.py +0 -0
  203. xfintech/data/source/tushare/stock/stockdividend/tests/test_class_stockdividend_all.py +725 -0
  204. xfintech/data/source/tushare/stock/stockinfo/__init__.py +19 -0
  205. xfintech/data/source/tushare/stock/stockinfo/constant.py +104 -0
  206. xfintech/data/source/tushare/stock/stockinfo/stockinfo.py +208 -0
  207. xfintech/data/source/tushare/stock/stockinfo/tests/__init__.py +0 -0
  208. xfintech/data/source/tushare/stock/stockinfo/tests/test_class_stockinfo_all.py +881 -0
  209. xfintech/data/source/tushare/stock/stockipo/__init__.py +19 -0
  210. xfintech/data/source/tushare/stock/stockipo/constant.py +90 -0
  211. xfintech/data/source/tushare/stock/stockipo/stockipo.py +234 -0
  212. xfintech/data/source/tushare/stock/stockipo/tests/__init__.py +1 -0
  213. xfintech/data/source/tushare/stock/stockipo/tests/test_class_stockipo_all.py +750 -0
  214. xfintech/data/source/tushare/stock/stockpledge/__init__.py +19 -0
  215. xfintech/data/source/tushare/stock/stockpledge/constant.py +72 -0
  216. xfintech/data/source/tushare/stock/stockpledge/stockpledge.py +158 -0
  217. xfintech/data/source/tushare/stock/stockpledge/tests/__init__.py +0 -0
  218. xfintech/data/source/tushare/stock/stockpledge/tests/test_class_stockpledge_all.py +664 -0
  219. xfintech/data/source/tushare/stock/stockpledgedetail/__init__.py +21 -0
  220. xfintech/data/source/tushare/stock/stockpledgedetail/constant.py +85 -0
  221. xfintech/data/source/tushare/stock/stockpledgedetail/stockpledgedetail.py +171 -0
  222. xfintech/data/source/tushare/stock/stockpledgedetail/tests/__init__.py +0 -0
  223. xfintech/data/source/tushare/stock/stockpledgedetail/tests/test_class_stockpledgedetail_all.py +112 -0
  224. xfintech/data/source/tushare/stock/stockst/__init__.py +19 -0
  225. xfintech/data/source/tushare/stock/stockst/constant.py +80 -0
  226. xfintech/data/source/tushare/stock/stockst/stockst.py +189 -0
  227. xfintech/data/source/tushare/stock/stockst/tests/__init__.py +0 -0
  228. xfintech/data/source/tushare/stock/stockst/tests/test_class_stockst_all.py +693 -0
  229. xfintech/data/source/tushare/stock/stocksuspend/__init__.py +21 -0
  230. xfintech/data/source/tushare/stock/stocksuspend/constant.py +75 -0
  231. xfintech/data/source/tushare/stock/stocksuspend/stocksuspend.py +151 -0
  232. xfintech/data/source/tushare/stock/stocksuspend/tests/__init__.py +0 -0
  233. xfintech/data/source/tushare/stock/stocksuspend/tests/test_class_stocksuspend_all.py +626 -0
  234. xfintech/data/source/tushare/stock/techindex/__init__.py +19 -0
  235. xfintech/data/source/tushare/stock/techindex/constant.py +600 -0
  236. xfintech/data/source/tushare/stock/techindex/techindex.py +314 -0
  237. xfintech/data/source/tushare/stock/techindex/tests/__init__.py +0 -0
  238. xfintech/data/source/tushare/stock/techindex/tests/test_class_techindex_all.py +576 -0
  239. xfintech/data/source/tushare/stock/tradedate/__init__.py +19 -0
  240. xfintech/data/source/tushare/stock/tradedate/constant.py +93 -0
  241. xfintech/data/source/tushare/stock/tradedate/tests/__init__.py +0 -0
  242. xfintech/data/source/tushare/stock/tradedate/tests/test_class_tradedate_all.py +947 -0
  243. xfintech/data/source/tushare/stock/tradedate/tradedate.py +234 -0
  244. xfintech/data/source/tushare/stock/weekline/__init__.py +19 -0
  245. xfintech/data/source/tushare/stock/weekline/constant.py +87 -0
  246. xfintech/data/source/tushare/stock/weekline/tests/__init__.py +0 -0
  247. xfintech/data/source/tushare/stock/weekline/tests/test_class_weekline_all.py +575 -0
  248. xfintech/data/source/tushare/stock/weekline/weekline.py +182 -0
  249. xfintech/fabric/__init__.py +18 -0
  250. xfintech/fabric/column/__init__.py +7 -0
  251. xfintech/fabric/column/info.py +202 -0
  252. xfintech/fabric/column/kind.py +102 -0
  253. xfintech/fabric/column/tests/__init__.py +0 -0
  254. xfintech/fabric/column/tests/test_class_info_all.py +207 -0
  255. xfintech/fabric/column/tests/test_class_kind_all.py +80 -0
  256. xfintech/fabric/table/__init__.py +5 -0
  257. xfintech/fabric/table/info.py +263 -0
  258. xfintech/fabric/table/tests/__init__.py +0 -0
  259. xfintech/fabric/table/tests/test_class_info_all.py +547 -0
  260. xfintech/serde/__init__.py +35 -0
  261. xfintech/serde/common/__init__.py +9 -0
  262. xfintech/serde/common/dataformat.py +78 -0
  263. xfintech/serde/common/deserialiserlike.py +38 -0
  264. xfintech/serde/common/error.py +182 -0
  265. xfintech/serde/common/serialiserlike.py +38 -0
  266. xfintech/serde/common/tests/__init__.py +1 -0
  267. xfintech/serde/common/tests/test_class_dataformat_all.py +694 -0
  268. xfintech/serde/common/tests/test_class_deserialiserlike_all.py +500 -0
  269. xfintech/serde/common/tests/test_class_errors_all.py +518 -0
  270. xfintech/serde/common/tests/test_class_serialiserlike_all.py +401 -0
  271. xfintech/serde/deserialiser/__init__.py +7 -0
  272. xfintech/serde/deserialiser/pandas.py +113 -0
  273. xfintech/serde/deserialiser/python.py +68 -0
  274. xfintech/serde/deserialiser/tests/__init__.py +1 -0
  275. xfintech/serde/deserialiser/tests/test_class_pandasdeserialiser_all.py +503 -0
  276. xfintech/serde/deserialiser/tests/test_class_pythondeserialiser_all.py +570 -0
  277. xfintech/serde/serialiser/__init__.py +7 -0
  278. xfintech/serde/serialiser/pandas.py +116 -0
  279. xfintech/serde/serialiser/python.py +71 -0
  280. xfintech/serde/serialiser/tests/__init__.py +1 -0
  281. xfintech/serde/serialiser/tests/test_class_pandasserialiser_all.py +474 -0
  282. 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