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