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,719 @@
1
+ """
2
+ Test suite for TushareRelayClient and RelayConnection classes
3
+ Tests cover initialization, authentication, API calls, and health checks
4
+ """
5
+
6
+ import gzip
7
+ import json
8
+ from unittest.mock import Mock, patch
9
+
10
+ import pandas as pd
11
+ import pytest
12
+ import requests
13
+
14
+ from xfintech.data.source.tushare.session.relay import RelayConnection, TushareRelayClient
15
+
16
+ # ============================================================================
17
+ # TushareRelayClient Initialization Tests
18
+ # ============================================================================
19
+
20
+
21
+ def test_relay_client_init_basic():
22
+ """Test TushareRelayClient initialization with basic parameters"""
23
+ client = TushareRelayClient(
24
+ url="https://relay.example.com",
25
+ secret="test-secret",
26
+ )
27
+ assert client.url == "https://relay.example.com"
28
+ assert client.secret == "test-secret"
29
+ assert client.timeout == 180
30
+
31
+
32
+ def test_relay_client_init_custom_timeout():
33
+ """Test TushareRelayClient initialization with custom timeout"""
34
+ client = TushareRelayClient(
35
+ url="https://relay.example.com",
36
+ secret="test-secret",
37
+ timeout=300,
38
+ )
39
+ assert client.timeout == 300
40
+
41
+
42
+ def test_relay_client_init_strips_trailing_slash():
43
+ """Test TushareRelayClient strips trailing slash from URL"""
44
+ client = TushareRelayClient(
45
+ url="https://relay.example.com/",
46
+ secret="test-secret",
47
+ )
48
+ assert client.url == "https://relay.example.com"
49
+
50
+
51
+ def test_relay_client_init_multiple_trailing_slashes():
52
+ """Test TushareRelayClient strips multiple trailing slashes"""
53
+ client = TushareRelayClient(
54
+ url="https://relay.example.com///",
55
+ secret="test-secret",
56
+ )
57
+ assert client.url == "https://relay.example.com"
58
+
59
+
60
+ def test_relay_client_init_empty_url():
61
+ """Test TushareRelayClient raises error with empty URL"""
62
+ with pytest.raises(ValueError, match="Relay URL must be provided"):
63
+ TushareRelayClient(
64
+ url="",
65
+ secret="test-secret",
66
+ )
67
+
68
+
69
+ def test_relay_client_init_none_url():
70
+ """Test TushareRelayClient raises error with None URL"""
71
+ with pytest.raises(ValueError, match="Relay URL must be provided"):
72
+ TushareRelayClient(
73
+ url=None,
74
+ secret="test-secret",
75
+ )
76
+
77
+
78
+ def test_relay_client_init_empty_secret():
79
+ """Test TushareRelayClient raises error with empty secret"""
80
+ with pytest.raises(ValueError, match="Relay secret must be provided"):
81
+ TushareRelayClient(url="https://relay.example.com", secret="")
82
+
83
+
84
+ def test_relay_client_init_none_secret():
85
+ """Test TushareRelayClient raises error with None secret"""
86
+ with pytest.raises(ValueError, match="Relay secret must be provided"):
87
+ TushareRelayClient(url="https://relay.example.com", secret=None)
88
+
89
+
90
+ def test_relay_client_init_strips_secret_whitespace():
91
+ """Test TushareRelayClient strips whitespace from secret"""
92
+ client = TushareRelayClient(
93
+ url="https://relay.example.com",
94
+ secret=" test-secret ",
95
+ )
96
+ assert client.secret == "test-secret"
97
+
98
+
99
+ # ============================================================================
100
+ # TushareRelayClient Resolve Methods Tests
101
+ # ============================================================================
102
+
103
+
104
+ def test_relay_client_resolve_timeout_default():
105
+ """Test _resolve_timeout returns default when None"""
106
+ client = TushareRelayClient(
107
+ url="https://relay.example.com",
108
+ secret="test-secret",
109
+ )
110
+ assert client._resolve_timeout(None) == 180
111
+
112
+
113
+ def test_relay_client_resolve_timeout_valid():
114
+ """Test _resolve_timeout returns valid timeout"""
115
+ client = TushareRelayClient(
116
+ url="https://relay.example.com",
117
+ secret="test-secret",
118
+ )
119
+ assert client._resolve_timeout(300) == 300
120
+
121
+
122
+ def test_relay_client_resolve_timeout_invalid_type():
123
+ """Test _resolve_timeout raises error with invalid type"""
124
+ client = TushareRelayClient(
125
+ url="https://relay.example.com",
126
+ secret="test-secret",
127
+ )
128
+ with pytest.raises(ValueError, match="Timeout must be an integer"):
129
+ client._resolve_timeout("invalid")
130
+
131
+
132
+ def test_relay_client_resolve_timeout_zero():
133
+ """Test _resolve_timeout returns default when zero"""
134
+ client = TushareRelayClient(
135
+ url="https://relay.example.com",
136
+ secret="test-secret",
137
+ )
138
+ assert client._resolve_timeout(0) == 180
139
+
140
+
141
+ def test_relay_client_resolve_timeout_negative():
142
+ """Test _resolve_timeout returns default when negative"""
143
+ client = TushareRelayClient(
144
+ url="https://relay.example.com",
145
+ secret="test-secret",
146
+ )
147
+ assert client._resolve_timeout(-100) == 180
148
+
149
+
150
+ # ============================================================================
151
+ # TushareRelayClient Canonical JSON Tests
152
+ # ============================================================================
153
+
154
+
155
+ def test_relay_client_canonical_json_simple():
156
+ """Test canonical_json with simple data"""
157
+ client = TushareRelayClient(
158
+ url="https://relay.example.com",
159
+ secret="test-secret",
160
+ )
161
+ result = client.canonical_json({"key": "value"})
162
+ assert isinstance(result, bytes)
163
+ assert result == b'{"key":"value"}'
164
+
165
+
166
+ def test_relay_client_canonical_json_sorted_keys():
167
+ """Test canonical_json sorts keys"""
168
+ client = TushareRelayClient(
169
+ url="https://relay.example.com",
170
+ secret="test-secret",
171
+ )
172
+ result = client.canonical_json({"z": 1, "a": 2, "m": 3})
173
+ assert result == b'{"a":2,"m":3,"z":1}'
174
+
175
+
176
+ def test_relay_client_canonical_json_no_spaces():
177
+ """Test canonical_json has no spaces"""
178
+ client = TushareRelayClient(
179
+ url="https://relay.example.com",
180
+ secret="test-secret",
181
+ )
182
+ result = client.canonical_json({"key": "value", "num": 42})
183
+ assert b" " not in result
184
+
185
+
186
+ def test_relay_client_canonical_json_unicode():
187
+ """Test canonical_json handles unicode"""
188
+ client = TushareRelayClient(
189
+ url="https://relay.example.com",
190
+ secret="test-secret",
191
+ )
192
+ result = client.canonical_json({"中文": "测试"})
193
+ assert "中文" in result.decode("utf-8")
194
+
195
+
196
+ def test_relay_client_canonical_json_nested():
197
+ """Test canonical_json with nested structure"""
198
+ client = TushareRelayClient(
199
+ url="https://relay.example.com",
200
+ secret="test-secret",
201
+ )
202
+ data = {"outer": {"inner": {"deep": "value"}}}
203
+ result = client.canonical_json(data)
204
+ assert result == b'{"outer":{"inner":{"deep":"value"}}}'
205
+
206
+
207
+ # ============================================================================
208
+ # TushareRelayClient Call Method Tests
209
+ # ============================================================================
210
+
211
+
212
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
213
+ @patch("xfintech.data.source.tushare.session.relay.pd.read_parquet")
214
+ def test_relay_client_call_basic(mock_read_parquet, mock_post):
215
+ """Test call method with basic parameters"""
216
+ # Setup mocks
217
+ mock_df = pd.DataFrame({"col1": [1, 2], "col2": ["a", "b"]})
218
+ mock_read_parquet.return_value = mock_df
219
+
220
+ mock_response = Mock()
221
+ mock_response.content = gzip.compress(b"parquet_data")
222
+ mock_post.return_value = mock_response
223
+
224
+ # Create client and call
225
+ client = TushareRelayClient(
226
+ url="https://relay.example.com",
227
+ secret="test-secret",
228
+ )
229
+
230
+ result = client.call(
231
+ method="daily",
232
+ limit=100,
233
+ offset=0,
234
+ params={"ts_code": "000001.SZ"},
235
+ )
236
+
237
+ # Verify
238
+ assert isinstance(result, pd.DataFrame)
239
+ assert mock_post.called
240
+ assert mock_read_parquet.called
241
+
242
+
243
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
244
+ def test_relay_client_call_correct_url(mock_post):
245
+ """Test call method uses correct URL"""
246
+ mock_response = Mock()
247
+ mock_response.content = gzip.compress(b"parquet_data")
248
+ mock_post.return_value = mock_response
249
+
250
+ with patch("xfintech.data.source.tushare.session.relay.pd.read_parquet"):
251
+ client = TushareRelayClient(
252
+ url="https://relay.example.com",
253
+ secret="test-secret",
254
+ )
255
+
256
+ client.call(method="daily", limit=100, offset=0, params={})
257
+
258
+ call_args = mock_post.call_args
259
+ assert call_args[0][0] == "https://relay.example.com/v2/tushare/call"
260
+
261
+
262
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
263
+ def test_relay_client_call_correct_headers(mock_post):
264
+ """Test call method sends correct headers"""
265
+ mock_response = Mock()
266
+ mock_response.content = gzip.compress(b"parquet_data")
267
+ mock_post.return_value = mock_response
268
+
269
+ with patch("xfintech.data.source.tushare.session.relay.pd.read_parquet"):
270
+ client = TushareRelayClient(
271
+ url="https://relay.example.com",
272
+ secret="test-secret",
273
+ )
274
+
275
+ client.call(method="daily", limit=100, offset=0, params={})
276
+
277
+ call_kwargs = mock_post.call_args[1]
278
+ headers = call_kwargs["headers"]
279
+
280
+ assert headers["Content-Type"] == "application/json"
281
+ assert "X-YNONCE" in headers
282
+ assert "X-YTS" in headers
283
+ assert "X-YSIGN" in headers
284
+ assert headers["X-Format"] == "parquet"
285
+ assert headers["X-Compression"] == "zstd+gzip"
286
+
287
+
288
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
289
+ def test_relay_client_call_with_none_limit_offset(mock_post):
290
+ """Test call method with None limit and offset"""
291
+ mock_response = Mock()
292
+ mock_response.content = gzip.compress(b"parquet_data")
293
+ mock_post.return_value = mock_response
294
+
295
+ with patch("xfintech.data.source.tushare.session.relay.pd.read_parquet"):
296
+ client = TushareRelayClient(
297
+ url="https://relay.example.com",
298
+ secret="test-secret",
299
+ )
300
+
301
+ client.call(method="daily", limit=None, offset=None, params={})
302
+
303
+ call_kwargs = mock_post.call_args[1]
304
+ body = json.loads(call_kwargs["data"])
305
+ assert body["limit"] is None
306
+ assert body["offset"] is None
307
+
308
+
309
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
310
+ def test_relay_client_call_timeout(mock_post):
311
+ """Test call method uses correct timeout"""
312
+ mock_response = Mock()
313
+ mock_response.content = gzip.compress(b"parquet_data")
314
+ mock_post.return_value = mock_response
315
+
316
+ with patch("xfintech.data.source.tushare.session.relay.pd.read_parquet"):
317
+ client = TushareRelayClient(
318
+ url="https://relay.example.com",
319
+ secret="test-secret",
320
+ timeout=300,
321
+ )
322
+
323
+ client.call(method="daily", limit=100, offset=0, params={})
324
+
325
+ call_kwargs = mock_post.call_args[1]
326
+ assert call_kwargs["timeout"] == 300
327
+
328
+
329
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
330
+ def test_relay_client_call_http_error(mock_post):
331
+ """Test call method raises error on HTTP error"""
332
+ mock_response = Mock()
333
+ mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
334
+ mock_post.return_value = mock_response
335
+
336
+ client = TushareRelayClient(
337
+ url="https://relay.example.com",
338
+ secret="test-secret",
339
+ )
340
+
341
+ with pytest.raises(requests.HTTPError):
342
+ client.call(method="daily", limit=100, offset=0, params={})
343
+
344
+
345
+ # ============================================================================
346
+ # TushareRelayClient Check Health Tests
347
+ # ============================================================================
348
+
349
+
350
+ @patch("xfintech.data.source.tushare.session.relay.requests.get")
351
+ def test_relay_client_check_health_ok(mock_get):
352
+ """Test check_health returns True when status is ok"""
353
+ mock_response = Mock()
354
+ mock_response.json.return_value = {"status": "ok"}
355
+ mock_get.return_value = mock_response
356
+
357
+ client = TushareRelayClient(
358
+ url="https://relay.example.com",
359
+ secret="test-secret",
360
+ )
361
+
362
+ result = client.check_health()
363
+ assert result is True
364
+ mock_get.assert_called_once_with("https://relay.example.com/health", timeout=180)
365
+
366
+
367
+ @patch("xfintech.data.source.tushare.session.relay.requests.get")
368
+ def test_relay_client_check_health_not_ok(mock_get):
369
+ """Test check_health raises error when status is not ok"""
370
+ mock_response = Mock()
371
+ mock_response.json.return_value = {"status": "error"}
372
+ mock_get.return_value = mock_response
373
+
374
+ client = TushareRelayClient(
375
+ url="https://relay.example.com",
376
+ secret="test-secret",
377
+ )
378
+
379
+ with pytest.raises(RuntimeError, match="Health check returned non-ok status"):
380
+ client.check_health()
381
+
382
+
383
+ @patch("xfintech.data.source.tushare.session.relay.requests.get")
384
+ def test_relay_client_check_health_connection_error(mock_get):
385
+ """Test check_health raises error on connection error"""
386
+ mock_get.side_effect = requests.ConnectionError("Connection failed")
387
+
388
+ client = TushareRelayClient(
389
+ url="https://relay.example.com",
390
+ secret="test-secret",
391
+ )
392
+
393
+ with pytest.raises(RuntimeError, match="Health check failed"):
394
+ client.check_health()
395
+
396
+
397
+ @patch("xfintech.data.source.tushare.session.relay.requests.get")
398
+ def test_relay_client_check_health_timeout_error(mock_get):
399
+ """Test check_health raises error on timeout"""
400
+ mock_get.side_effect = requests.Timeout("Request timed out")
401
+
402
+ client = TushareRelayClient(
403
+ url="https://relay.example.com",
404
+ secret="test-secret",
405
+ )
406
+
407
+ with pytest.raises(RuntimeError, match="Health check failed"):
408
+ client.check_health()
409
+
410
+
411
+ @patch("xfintech.data.source.tushare.session.relay.requests.get")
412
+ def test_relay_client_check_health_custom_timeout(mock_get):
413
+ """Test check_health uses custom timeout"""
414
+ mock_response = Mock()
415
+ mock_response.json.return_value = {"status": "ok"}
416
+ mock_get.return_value = mock_response
417
+
418
+ client = TushareRelayClient(
419
+ url="https://relay.example.com",
420
+ secret="test-secret",
421
+ timeout=60,
422
+ )
423
+
424
+ client.check_health()
425
+
426
+ call_kwargs = mock_get.call_args[1]
427
+ assert call_kwargs["timeout"] == 60
428
+
429
+
430
+ # ============================================================================
431
+ # RelayConnection Initialization Tests
432
+ # ============================================================================
433
+
434
+
435
+ def test_relay_connection_init():
436
+ """Test RelayConnection initialization"""
437
+ client = TushareRelayClient(
438
+ url="https://relay.example.com",
439
+ secret="test-secret",
440
+ )
441
+ connection = RelayConnection(client=client)
442
+ assert connection.client is client
443
+
444
+
445
+ # ============================================================================
446
+ # RelayConnection Dynamic Method Tests
447
+ # ============================================================================
448
+
449
+
450
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
451
+ @patch("xfintech.data.source.tushare.session.relay.pd.read_parquet")
452
+ def test_relay_connection_dynamic_method_call(mock_read_parquet, mock_post):
453
+ """Test RelayConnection dynamic method call"""
454
+ mock_df = pd.DataFrame({"col1": [1, 2]})
455
+ mock_read_parquet.return_value = mock_df
456
+
457
+ mock_response = Mock()
458
+ mock_response.content = gzip.compress(b"parquet_data")
459
+ mock_post.return_value = mock_response
460
+
461
+ client = TushareRelayClient(
462
+ url="https://relay.example.com",
463
+ secret="test-secret",
464
+ )
465
+ connection = RelayConnection(client=client)
466
+
467
+ # Call dynamic method
468
+ result = connection.daily(ts_code="000001.SZ", limit=100, offset=0)
469
+
470
+ assert isinstance(result, pd.DataFrame)
471
+
472
+
473
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
474
+ @patch("xfintech.data.source.tushare.session.relay.pd.read_parquet")
475
+ def test_relay_connection_multiple_methods(mock_read_parquet, mock_post):
476
+ """Test RelayConnection can call multiple different methods"""
477
+ mock_df = pd.DataFrame({"col1": [1, 2]})
478
+ mock_read_parquet.return_value = mock_df
479
+
480
+ mock_response = Mock()
481
+ mock_response.content = gzip.compress(b"parquet_data")
482
+ mock_post.return_value = mock_response
483
+
484
+ client = TushareRelayClient(
485
+ url="https://relay.example.com",
486
+ secret="test-secret",
487
+ )
488
+ connection = RelayConnection(client=client)
489
+
490
+ # Call different methods
491
+ connection.daily(ts_code="000001.SZ")
492
+ connection.stock_basic(exchange="SSE")
493
+ connection.trade_cal(exchange="SSE")
494
+
495
+ assert mock_post.call_count == 3
496
+
497
+
498
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
499
+ @patch("xfintech.data.source.tushare.session.relay.pd.read_parquet")
500
+ def test_relay_connection_keyword_only_params(mock_read_parquet, mock_post):
501
+ """Test RelayConnection enforces keyword-only parameters"""
502
+ mock_df = pd.DataFrame({"col1": [1, 2]})
503
+ mock_read_parquet.return_value = mock_df
504
+
505
+ mock_response = Mock()
506
+ mock_response.content = gzip.compress(b"parquet_data")
507
+ mock_post.return_value = mock_response
508
+
509
+ client = TushareRelayClient(
510
+ url="https://relay.example.com",
511
+ secret="test-secret",
512
+ )
513
+ connection = RelayConnection(client=client)
514
+
515
+ # Should work with keywords
516
+ result = connection.daily(ts_code="000001.SZ", limit=100)
517
+ assert isinstance(result, pd.DataFrame)
518
+
519
+
520
+ # ============================================================================
521
+ # Integration Tests
522
+ # ============================================================================
523
+
524
+
525
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
526
+ @patch("xfintech.data.source.tushare.session.relay.pd.read_parquet")
527
+ def test_relay_full_workflow(mock_read_parquet, mock_post):
528
+ """Test complete relay workflow"""
529
+ mock_df = pd.DataFrame(
530
+ {
531
+ "ts_code": ["000001.SZ", "000002.SZ"],
532
+ "trade_date": ["20240101", "20240101"],
533
+ "close": [10.5, 20.3],
534
+ }
535
+ )
536
+ mock_read_parquet.return_value = mock_df
537
+
538
+ mock_response = Mock()
539
+ mock_response.content = gzip.compress(b"parquet_data")
540
+ mock_post.return_value = mock_response
541
+
542
+ # Create client and connection
543
+ client = TushareRelayClient(
544
+ url="https://relay.example.com",
545
+ secret="test-secret",
546
+ timeout=120,
547
+ )
548
+ connection = RelayConnection(client=client)
549
+
550
+ # Fetch data
551
+ result = connection.daily(
552
+ ts_code="000001.SZ",
553
+ start_date="20240101",
554
+ end_date="20240131",
555
+ limit=1000,
556
+ offset=0,
557
+ )
558
+
559
+ assert isinstance(result, pd.DataFrame)
560
+ assert len(result) == 2
561
+ assert "ts_code" in result.columns
562
+
563
+
564
+ def test_relay_client_constants():
565
+ """Test TushareRelayClient class constants"""
566
+ assert TushareRelayClient.DEFAULT_TIMEOUT == 180
567
+
568
+
569
+ # ============================================================================
570
+ # TushareRelayClient Refresh Tests
571
+ # ============================================================================
572
+
573
+
574
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
575
+ def test_relay_client_refresh_success(mock_post):
576
+ """Test refresh method with successful response"""
577
+ mock_response = Mock()
578
+ mock_response.json.return_value = {"status": "ok", "message": "Refreshed"}
579
+ mock_post.return_value = mock_response
580
+
581
+ client = TushareRelayClient(
582
+ url="https://relay.example.com",
583
+ secret="test-secret",
584
+ )
585
+ result = client.refresh()
586
+
587
+ assert result is True
588
+ mock_post.assert_called_once()
589
+
590
+ # Verify the endpoint
591
+ call_args = mock_post.call_args
592
+ assert call_args[0][0] == "https://relay.example.com/v2/tushare/refresh"
593
+
594
+ # Verify headers
595
+ headers = call_args[1]["headers"]
596
+ assert "X-YNONCE" in headers
597
+ assert "X-YTS" in headers
598
+ assert "X-YSIGN" in headers
599
+ assert headers["Content-Type"] == "application/json"
600
+
601
+
602
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
603
+ def test_relay_client_refresh_non_ok_status(mock_post):
604
+ """Test refresh method raises error on non-ok status"""
605
+ mock_response = Mock()
606
+ mock_response.json.return_value = {"status": "error", "message": "Failed"}
607
+ mock_post.return_value = mock_response
608
+
609
+ client = TushareRelayClient(
610
+ url="https://relay.example.com",
611
+ secret="test-secret",
612
+ )
613
+
614
+ with pytest.raises(RuntimeError, match="Refresh returned non-ok status"):
615
+ client.refresh()
616
+
617
+
618
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
619
+ def test_relay_client_refresh_http_error(mock_post):
620
+ """Test refresh method handles HTTP errors"""
621
+ mock_post.side_effect = requests.exceptions.HTTPError("500 Server Error")
622
+
623
+ client = TushareRelayClient(
624
+ url="https://relay.example.com",
625
+ secret="test-secret",
626
+ )
627
+
628
+ with pytest.raises(RuntimeError, match="Tushare refresh failed"):
629
+ client.refresh()
630
+
631
+
632
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
633
+ def test_relay_client_refresh_timeout(mock_post):
634
+ """Test refresh method handles timeout errors"""
635
+ mock_post.side_effect = requests.exceptions.Timeout("Request timeout")
636
+
637
+ client = TushareRelayClient(
638
+ url="https://relay.example.com",
639
+ secret="test-secret",
640
+ timeout=5,
641
+ )
642
+
643
+ with pytest.raises(RuntimeError, match="Tushare refresh failed"):
644
+ client.refresh()
645
+
646
+
647
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
648
+ def test_relay_client_refresh_uses_correct_timeout(mock_post):
649
+ """Test refresh method uses client timeout setting"""
650
+ mock_response = Mock()
651
+ mock_response.json.return_value = {"status": "ok"}
652
+ mock_post.return_value = mock_response
653
+
654
+ client = TushareRelayClient(
655
+ url="https://relay.example.com",
656
+ secret="test-secret",
657
+ timeout=300,
658
+ )
659
+ client.refresh()
660
+
661
+ call_args = mock_post.call_args
662
+ assert call_args[1]["timeout"] == 300
663
+
664
+
665
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
666
+ def test_relay_client_refresh_authentication(mock_post):
667
+ """Test refresh method sends proper authentication"""
668
+ mock_response = Mock()
669
+ mock_response.json.return_value = {"status": "ok"}
670
+ mock_post.return_value = mock_response
671
+
672
+ client = TushareRelayClient(
673
+ url="https://relay.example.com",
674
+ secret="my-secret-key",
675
+ )
676
+ client.refresh()
677
+
678
+ # Verify authentication headers are present
679
+ call_args = mock_post.call_args
680
+ headers = call_args[1]["headers"]
681
+
682
+ assert len(headers["X-YNONCE"]) == 32 # 16 bytes hex = 32 chars
683
+ assert headers["X-YTS"].isdigit()
684
+ assert len(headers["X-YSIGN"]) == 64 # SHA256 hex = 64 chars
685
+
686
+
687
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
688
+ def test_relay_client_refresh_empty_payload(mock_post):
689
+ """Test refresh method sends empty payload"""
690
+ mock_response = Mock()
691
+ mock_response.json.return_value = {"status": "ok"}
692
+ mock_post.return_value = mock_response
693
+
694
+ client = TushareRelayClient(
695
+ url="https://relay.example.com",
696
+ secret="test-secret",
697
+ )
698
+ client.refresh()
699
+
700
+ # Verify empty JSON payload
701
+ call_args = mock_post.call_args
702
+ data = call_args[1]["data"]
703
+ assert data == b"{}"
704
+
705
+
706
+ @patch("xfintech.data.source.tushare.session.relay.requests.post")
707
+ def test_relay_client_refresh_json_decode_error(mock_post):
708
+ """Test refresh method handles JSON decode errors"""
709
+ mock_response = Mock()
710
+ mock_response.json.side_effect = ValueError("Invalid JSON")
711
+ mock_post.return_value = mock_response
712
+
713
+ client = TushareRelayClient(
714
+ url="https://relay.example.com",
715
+ secret="test-secret",
716
+ )
717
+
718
+ with pytest.raises(RuntimeError, match="Tushare refresh failed"):
719
+ client.refresh()