lib-x17-fintech 2.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lib_x17_fintech-2.1.3.dist-info/METADATA +633 -0
- lib_x17_fintech-2.1.3.dist-info/RECORD +282 -0
- lib_x17_fintech-2.1.3.dist-info/WHEEL +5 -0
- lib_x17_fintech-2.1.3.dist-info/licenses/LICENSE +1 -0
- lib_x17_fintech-2.1.3.dist-info/top_level.txt +1 -0
- xfintech/__init__.py +0 -0
- xfintech/connect/__init__.py +18 -0
- xfintech/connect/artifact/__init__.py +5 -0
- xfintech/connect/artifact/artifact.py +168 -0
- xfintech/connect/artifact/tests/__init__.py +3 -0
- xfintech/connect/artifact/tests/test_class_artifact_all.py +564 -0
- xfintech/connect/common/__init__.py +12 -0
- xfintech/connect/common/connect.py +49 -0
- xfintech/connect/common/connectref.py +119 -0
- xfintech/connect/common/error.py +62 -0
- xfintech/connect/common/tests/__init__.py +1 -0
- xfintech/connect/common/tests/test_class_connectlike_all.py +544 -0
- xfintech/connect/common/tests/test_class_connectref_all.py +586 -0
- xfintech/connect/common/tests/test_class_errors_all.py +524 -0
- xfintech/connect/instance/__init__.py +7 -0
- xfintech/connect/instance/macos.py +121 -0
- xfintech/connect/instance/s3.py +176 -0
- xfintech/connect/instance/tests/__init__.py +1 -0
- xfintech/connect/instance/tests/test_class_macosconnect_all.py +692 -0
- xfintech/connect/instance/tests/test_class_s3connect_all.py +603 -0
- xfintech/data/__init__.py +20 -0
- xfintech/data/common/__init__.py +15 -0
- xfintech/data/common/cache.py +186 -0
- xfintech/data/common/coolant.py +171 -0
- xfintech/data/common/metric.py +138 -0
- xfintech/data/common/paginate.py +132 -0
- xfintech/data/common/params.py +162 -0
- xfintech/data/common/retry.py +201 -0
- xfintech/data/common/tests/__init__.py +1 -0
- xfintech/data/common/tests/test_class_cache_all.py +681 -0
- xfintech/data/common/tests/test_class_coolant_all.py +534 -0
- xfintech/data/common/tests/test_class_metric_all.py +705 -0
- xfintech/data/common/tests/test_class_paginate_all.py +508 -0
- xfintech/data/common/tests/test_class_params_all.py +891 -0
- xfintech/data/common/tests/test_class_retry_all.py +714 -0
- xfintech/data/job/__init__.py +17 -0
- xfintech/data/job/errors.py +112 -0
- xfintech/data/job/house.py +156 -0
- xfintech/data/job/job.py +247 -0
- xfintech/data/job/joblike.py +47 -0
- xfintech/data/job/tests/__init__.py +1 -0
- xfintech/data/job/tests/test_class_errors_all.py +275 -0
- xfintech/data/job/tests/test_class_house_all.py +801 -0
- xfintech/data/job/tests/test_class_job_all.py +684 -0
- xfintech/data/job/tests/test_class_joblike_all.py +482 -0
- xfintech/data/relay/__init__.py +7 -0
- xfintech/data/relay/client.py +114 -0
- xfintech/data/relay/clientlike.py +45 -0
- xfintech/data/relay/tests/test_class_relayclient_all.py +484 -0
- xfintech/data/relay/tests/test_class_relayclientlike_all.py +500 -0
- xfintech/data/source/__init__.py +7 -0
- xfintech/data/source/baostock/__init__.py +21 -0
- xfintech/data/source/baostock/job/__init__.py +5 -0
- xfintech/data/source/baostock/job/job.py +217 -0
- xfintech/data/source/baostock/job/tests/__init__.py +0 -0
- xfintech/data/source/baostock/job/tests/test_class_baostockjob_all.py +547 -0
- xfintech/data/source/baostock/session/__init__.py +8 -0
- xfintech/data/source/baostock/session/relay.py +223 -0
- xfintech/data/source/baostock/session/session.py +241 -0
- xfintech/data/source/baostock/session/tests/__init__.py +0 -0
- xfintech/data/source/baostock/session/tests/test_class_relay_all.py +694 -0
- xfintech/data/source/baostock/session/tests/test_class_session_all.py +505 -0
- xfintech/data/source/baostock/stock/__init__.py +0 -0
- xfintech/data/source/baostock/stock/hs300stock/__init__.py +3 -0
- xfintech/data/source/baostock/stock/hs300stock/constant.py +49 -0
- xfintech/data/source/baostock/stock/hs300stock/hs300stock.py +133 -0
- xfintech/data/source/baostock/stock/hs300stock/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/hs300stock/tests/test_class_hs300index_all.py +413 -0
- xfintech/data/source/baostock/stock/minuteline/__init__.py +19 -0
- xfintech/data/source/baostock/stock/minuteline/constant.py +89 -0
- xfintech/data/source/baostock/stock/minuteline/minuteline.py +163 -0
- xfintech/data/source/baostock/stock/minuteline/tests/__init__.py +0 -0
- xfintech/data/source/baostock/stock/minuteline/tests/test_class_minuteline_all.py +582 -0
- xfintech/data/source/baostock/stock/stock/__init__.py +19 -0
- xfintech/data/source/baostock/stock/stock/constant.py +55 -0
- xfintech/data/source/baostock/stock/stock/stock.py +149 -0
- xfintech/data/source/baostock/stock/stock/tests/__init__.py +0 -0
- xfintech/data/source/baostock/stock/stock/tests/test_class_stock_all.py +508 -0
- xfintech/data/source/baostock/stock/stockinfo/__init__.py +5 -0
- xfintech/data/source/baostock/stock/stockinfo/constant.py +66 -0
- xfintech/data/source/baostock/stock/stockinfo/stockinfo.py +176 -0
- xfintech/data/source/baostock/stock/stockinfo/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/stockinfo/tests/test_class_stockinfo_all.py +617 -0
- xfintech/data/source/baostock/stock/sz50stock/__init__.py +3 -0
- xfintech/data/source/baostock/stock/sz50stock/constant.py +49 -0
- xfintech/data/source/baostock/stock/sz50stock/sz50stock.py +133 -0
- xfintech/data/source/baostock/stock/sz50stock/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/sz50stock/tests/test_class_sz50stock_all.py +397 -0
- xfintech/data/source/baostock/stock/tradedate/__init__.py +19 -0
- xfintech/data/source/baostock/stock/tradedate/constant.py +72 -0
- xfintech/data/source/baostock/stock/tradedate/tests/__init__.py +0 -0
- xfintech/data/source/baostock/stock/tradedate/tests/test_class_tradedate_all.py +695 -0
- xfintech/data/source/baostock/stock/tradedate/tradedate.py +208 -0
- xfintech/data/source/baostock/stock/zz500stock/__init__.py +3 -0
- xfintech/data/source/baostock/stock/zz500stock/constant.py +55 -0
- xfintech/data/source/baostock/stock/zz500stock/tests/__init__.py +1 -0
- xfintech/data/source/baostock/stock/zz500stock/tests/test_class_zz500stock_all.py +421 -0
- xfintech/data/source/baostock/stock/zz500stock/zz500stock.py +133 -0
- xfintech/data/source/tushare/__init__.py +61 -0
- xfintech/data/source/tushare/job/__init__.py +5 -0
- xfintech/data/source/tushare/job/job.py +257 -0
- xfintech/data/source/tushare/job/tests/test_class_tusharejob_all.py +589 -0
- xfintech/data/source/tushare/session/__init__.py +5 -0
- xfintech/data/source/tushare/session/relay.py +231 -0
- xfintech/data/source/tushare/session/session.py +239 -0
- xfintech/data/source/tushare/session/tests/test_class_relay_all.py +719 -0
- xfintech/data/source/tushare/session/tests/test_class_session_all.py +705 -0
- xfintech/data/source/tushare/stock/__init__.py +55 -0
- xfintech/data/source/tushare/stock/adjfactor/__init__.py +19 -0
- xfintech/data/source/tushare/stock/adjfactor/adjfactor.py +150 -0
- xfintech/data/source/tushare/stock/adjfactor/constant.py +71 -0
- xfintech/data/source/tushare/stock/adjfactor/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/adjfactor/tests/test_class_adjfactor_all.py +372 -0
- xfintech/data/source/tushare/stock/capflow/__init__.py +19 -0
- xfintech/data/source/tushare/stock/capflow/capflow.py +171 -0
- xfintech/data/source/tushare/stock/capflow/constant.py +105 -0
- xfintech/data/source/tushare/stock/capflow/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/capflow/tests/test_class_capflow_all.py +589 -0
- xfintech/data/source/tushare/stock/capflowdc/__init__.py +19 -0
- xfintech/data/source/tushare/stock/capflowdc/capflowdc.py +167 -0
- xfintech/data/source/tushare/stock/capflowdc/constant.py +95 -0
- xfintech/data/source/tushare/stock/capflowdc/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/capflowdc/tests/test_class_capflowdc_all.py +814 -0
- xfintech/data/source/tushare/stock/capflowths/__init__.py +19 -0
- xfintech/data/source/tushare/stock/capflowths/capflowths.py +173 -0
- xfintech/data/source/tushare/stock/capflowths/constant.py +92 -0
- xfintech/data/source/tushare/stock/capflowths/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/capflowths/tests/test_class_capflowths_all.py +551 -0
- xfintech/data/source/tushare/stock/company/__init__.py +19 -0
- xfintech/data/source/tushare/stock/company/company.py +188 -0
- xfintech/data/source/tushare/stock/company/constant.py +92 -0
- xfintech/data/source/tushare/stock/company/tests/__init__.py +1 -0
- xfintech/data/source/tushare/stock/company/tests/test_class_company_all.py +829 -0
- xfintech/data/source/tushare/stock/companybusiness/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companybusiness/companybusiness.py +183 -0
- xfintech/data/source/tushare/stock/companybusiness/constant.py +91 -0
- xfintech/data/source/tushare/stock/companybusiness/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companybusiness/tests/test_class_companybusiness_all.py +633 -0
- xfintech/data/source/tushare/stock/companycashflow/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companycashflow/companycashflow.py +277 -0
- xfintech/data/source/tushare/stock/companycashflow/constant.py +293 -0
- xfintech/data/source/tushare/stock/companycashflow/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companycashflow/tests/test_class_companycashflow_all.py +619 -0
- xfintech/data/source/tushare/stock/companydebt/__init__.py +19 -0
- xfintech/data/source/tushare/stock/companydebt/companydebt.py +339 -0
- xfintech/data/source/tushare/stock/companydebt/constant.py +403 -0
- xfintech/data/source/tushare/stock/companydebt/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companydebt/tests/test_class_companydebt_all.py +655 -0
- xfintech/data/source/tushare/stock/companyoverview/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companyoverview/companyoverview.py +214 -0
- xfintech/data/source/tushare/stock/companyoverview/constant.py +152 -0
- xfintech/data/source/tushare/stock/companyoverview/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companyoverview/tests/test_class_companyoverview_all.py +647 -0
- xfintech/data/source/tushare/stock/companyprofit/__init__.py +21 -0
- xfintech/data/source/tushare/stock/companyprofit/companyprofit.py +272 -0
- xfintech/data/source/tushare/stock/companyprofit/constant.py +259 -0
- xfintech/data/source/tushare/stock/companyprofit/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/companyprofit/tests/test_class_companyprofit_all.py +635 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/__init__.py +21 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/conceptcapflowdc.py +175 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/constant.py +106 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/conceptcapflowdc/tests/test_class_conceptcapflowdc_all.py +568 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/__init__.py +21 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/conceptcapflowths.py +188 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/constant.py +89 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/conceptcapflowths/tests/test_class_conceptcapflowths_all.py +516 -0
- xfintech/data/source/tushare/stock/dayline/__init__.py +19 -0
- xfintech/data/source/tushare/stock/dayline/constant.py +87 -0
- xfintech/data/source/tushare/stock/dayline/dayline.py +177 -0
- xfintech/data/source/tushare/stock/dayline/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/dayline/tests/test_class_dayline_all.py +585 -0
- xfintech/data/source/tushare/stock/industrycapflowths/__init__.py +21 -0
- xfintech/data/source/tushare/stock/industrycapflowths/constant.py +89 -0
- xfintech/data/source/tushare/stock/industrycapflowths/industrycapflowths.py +192 -0
- xfintech/data/source/tushare/stock/industrycapflowths/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/industrycapflowths/tests/test_class_industrycapflowths_all.py +683 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/__init__.py +21 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/constant.py +90 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/marketindexcapflowdc.py +173 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/marketindexcapflowdc/tests/test_class_marketindexcapflowdc_all.py +793 -0
- xfintech/data/source/tushare/stock/monthline/__init__.py +19 -0
- xfintech/data/source/tushare/stock/monthline/constant.py +87 -0
- xfintech/data/source/tushare/stock/monthline/monthline.py +180 -0
- xfintech/data/source/tushare/stock/monthline/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/monthline/tests/test_class_monthline_all.py +574 -0
- xfintech/data/source/tushare/stock/stock/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stock/constant.py +105 -0
- xfintech/data/source/tushare/stock/stock/stock.py +193 -0
- xfintech/data/source/tushare/stock/stock/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stock/tests/test_class_stock_all.py +788 -0
- xfintech/data/source/tushare/stock/stockdividend/__init__.py +21 -0
- xfintech/data/source/tushare/stock/stockdividend/constant.py +111 -0
- xfintech/data/source/tushare/stock/stockdividend/stockdividend.py +180 -0
- xfintech/data/source/tushare/stock/stockdividend/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockdividend/tests/test_class_stockdividend_all.py +725 -0
- xfintech/data/source/tushare/stock/stockinfo/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockinfo/constant.py +104 -0
- xfintech/data/source/tushare/stock/stockinfo/stockinfo.py +208 -0
- xfintech/data/source/tushare/stock/stockinfo/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockinfo/tests/test_class_stockinfo_all.py +881 -0
- xfintech/data/source/tushare/stock/stockipo/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockipo/constant.py +90 -0
- xfintech/data/source/tushare/stock/stockipo/stockipo.py +234 -0
- xfintech/data/source/tushare/stock/stockipo/tests/__init__.py +1 -0
- xfintech/data/source/tushare/stock/stockipo/tests/test_class_stockipo_all.py +750 -0
- xfintech/data/source/tushare/stock/stockpledge/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockpledge/constant.py +72 -0
- xfintech/data/source/tushare/stock/stockpledge/stockpledge.py +158 -0
- xfintech/data/source/tushare/stock/stockpledge/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockpledge/tests/test_class_stockpledge_all.py +664 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/__init__.py +21 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/constant.py +85 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/stockpledgedetail.py +171 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockpledgedetail/tests/test_class_stockpledgedetail_all.py +112 -0
- xfintech/data/source/tushare/stock/stockst/__init__.py +19 -0
- xfintech/data/source/tushare/stock/stockst/constant.py +80 -0
- xfintech/data/source/tushare/stock/stockst/stockst.py +189 -0
- xfintech/data/source/tushare/stock/stockst/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stockst/tests/test_class_stockst_all.py +693 -0
- xfintech/data/source/tushare/stock/stocksuspend/__init__.py +21 -0
- xfintech/data/source/tushare/stock/stocksuspend/constant.py +75 -0
- xfintech/data/source/tushare/stock/stocksuspend/stocksuspend.py +151 -0
- xfintech/data/source/tushare/stock/stocksuspend/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/stocksuspend/tests/test_class_stocksuspend_all.py +626 -0
- xfintech/data/source/tushare/stock/techindex/__init__.py +19 -0
- xfintech/data/source/tushare/stock/techindex/constant.py +600 -0
- xfintech/data/source/tushare/stock/techindex/techindex.py +314 -0
- xfintech/data/source/tushare/stock/techindex/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/techindex/tests/test_class_techindex_all.py +576 -0
- xfintech/data/source/tushare/stock/tradedate/__init__.py +19 -0
- xfintech/data/source/tushare/stock/tradedate/constant.py +93 -0
- xfintech/data/source/tushare/stock/tradedate/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/tradedate/tests/test_class_tradedate_all.py +947 -0
- xfintech/data/source/tushare/stock/tradedate/tradedate.py +234 -0
- xfintech/data/source/tushare/stock/weekline/__init__.py +19 -0
- xfintech/data/source/tushare/stock/weekline/constant.py +87 -0
- xfintech/data/source/tushare/stock/weekline/tests/__init__.py +0 -0
- xfintech/data/source/tushare/stock/weekline/tests/test_class_weekline_all.py +575 -0
- xfintech/data/source/tushare/stock/weekline/weekline.py +182 -0
- xfintech/fabric/__init__.py +18 -0
- xfintech/fabric/column/__init__.py +7 -0
- xfintech/fabric/column/info.py +202 -0
- xfintech/fabric/column/kind.py +102 -0
- xfintech/fabric/column/tests/__init__.py +0 -0
- xfintech/fabric/column/tests/test_class_info_all.py +207 -0
- xfintech/fabric/column/tests/test_class_kind_all.py +80 -0
- xfintech/fabric/table/__init__.py +5 -0
- xfintech/fabric/table/info.py +263 -0
- xfintech/fabric/table/tests/__init__.py +0 -0
- xfintech/fabric/table/tests/test_class_info_all.py +547 -0
- xfintech/serde/__init__.py +35 -0
- xfintech/serde/common/__init__.py +9 -0
- xfintech/serde/common/dataformat.py +78 -0
- xfintech/serde/common/deserialiserlike.py +38 -0
- xfintech/serde/common/error.py +182 -0
- xfintech/serde/common/serialiserlike.py +38 -0
- xfintech/serde/common/tests/__init__.py +1 -0
- xfintech/serde/common/tests/test_class_dataformat_all.py +694 -0
- xfintech/serde/common/tests/test_class_deserialiserlike_all.py +500 -0
- xfintech/serde/common/tests/test_class_errors_all.py +518 -0
- xfintech/serde/common/tests/test_class_serialiserlike_all.py +401 -0
- xfintech/serde/deserialiser/__init__.py +7 -0
- xfintech/serde/deserialiser/pandas.py +113 -0
- xfintech/serde/deserialiser/python.py +68 -0
- xfintech/serde/deserialiser/tests/__init__.py +1 -0
- xfintech/serde/deserialiser/tests/test_class_pandasdeserialiser_all.py +503 -0
- xfintech/serde/deserialiser/tests/test_class_pythondeserialiser_all.py +570 -0
- xfintech/serde/serialiser/__init__.py +7 -0
- xfintech/serde/serialiser/pandas.py +116 -0
- xfintech/serde/serialiser/python.py +71 -0
- xfintech/serde/serialiser/tests/__init__.py +1 -0
- xfintech/serde/serialiser/tests/test_class_pandasserialiser_all.py +474 -0
- xfintech/serde/serialiser/tests/test_class_pythonserialiser_all.py +508 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""
|
|
2
|
+
描述:
|
|
3
|
+
- S3Connect 类的单元测试。
|
|
4
|
+
- 测试 S3 连接实现的所有功能和边界情况。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from xfintech.connect.common.connect import ConnectLike
|
|
14
|
+
from xfintech.connect.common.connectref import ConnectRef
|
|
15
|
+
from xfintech.connect.instance.s3 import S3Connect
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# Initialization Tests
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_s3connect_init_default_client():
|
|
23
|
+
"""测试使用默认客户端初始化"""
|
|
24
|
+
with patch("boto3.client") as mock_client_factory:
|
|
25
|
+
mock_client = MagicMock()
|
|
26
|
+
mock_client.put_object = MagicMock()
|
|
27
|
+
mock_client_factory.return_value = mock_client
|
|
28
|
+
|
|
29
|
+
connect = S3Connect()
|
|
30
|
+
|
|
31
|
+
assert connect.kind == "s3"
|
|
32
|
+
assert connect._client is mock_client
|
|
33
|
+
mock_client_factory.assert_called_once_with("s3")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_s3connect_init_custom_client():
|
|
37
|
+
"""测试使用自定义客户端初始化"""
|
|
38
|
+
mock_client = MagicMock()
|
|
39
|
+
mock_client.put_object = MagicMock()
|
|
40
|
+
|
|
41
|
+
connect = S3Connect(client=mock_client)
|
|
42
|
+
|
|
43
|
+
assert connect.kind == "s3"
|
|
44
|
+
assert connect._client is mock_client
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_s3connect_init_without_boto3():
|
|
48
|
+
"""测试没有 boto3 时初始化失败"""
|
|
49
|
+
with patch("builtins.__import__", side_effect=ImportError("No module named 'boto3'")):
|
|
50
|
+
with pytest.raises(ImportError) as excinfo:
|
|
51
|
+
S3Connect()
|
|
52
|
+
assert "boto3 is required" in str(excinfo.value)
|
|
53
|
+
assert "pip install boto3" in str(excinfo.value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_s3connect_is_connectlike():
|
|
57
|
+
"""测试 S3Connect 实现了 ConnectLike 协议"""
|
|
58
|
+
mock_client = MagicMock()
|
|
59
|
+
mock_client.put_object = MagicMock()
|
|
60
|
+
|
|
61
|
+
connect = S3Connect(client=mock_client)
|
|
62
|
+
assert isinstance(connect, ConnectLike)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_s3connect_has_docstring():
|
|
66
|
+
"""测试 S3Connect 有文档字符串"""
|
|
67
|
+
assert S3Connect.__doc__ is not None
|
|
68
|
+
assert len(S3Connect.__doc__) > 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# =============================================================================
|
|
72
|
+
# Client Validation Tests
|
|
73
|
+
# =============================================================================
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_resolve_client_valid_custom_client():
|
|
77
|
+
"""测试解析有效的自定义客户端"""
|
|
78
|
+
mock_client = MagicMock()
|
|
79
|
+
mock_client.put_object = MagicMock()
|
|
80
|
+
|
|
81
|
+
connect = S3Connect(client=mock_client)
|
|
82
|
+
assert connect._client is mock_client
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_resolve_client_missing_put_object():
|
|
86
|
+
"""测试客户端缺少 put_object 方法"""
|
|
87
|
+
with patch("boto3.client") as mock_client_factory:
|
|
88
|
+
mock_client = MagicMock(spec=[]) # No methods
|
|
89
|
+
mock_client_factory.return_value = mock_client
|
|
90
|
+
|
|
91
|
+
with pytest.raises(AttributeError) as excinfo:
|
|
92
|
+
S3Connect()
|
|
93
|
+
assert "missing put_object method" in str(excinfo.value)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_resolve_client_put_object_not_callable():
|
|
97
|
+
"""测试 put_object 不可调用"""
|
|
98
|
+
with patch("boto3.client") as mock_client_factory:
|
|
99
|
+
mock_client = MagicMock()
|
|
100
|
+
mock_client.put_object = "not_callable"
|
|
101
|
+
mock_client_factory.return_value = mock_client
|
|
102
|
+
|
|
103
|
+
with pytest.raises(TypeError) as excinfo:
|
|
104
|
+
S3Connect()
|
|
105
|
+
assert "put_object is not callable" in str(excinfo.value)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# =============================================================================
|
|
109
|
+
# URI Resolution Tests
|
|
110
|
+
# =============================================================================
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_resolve_s3_uri_full_uri():
|
|
114
|
+
"""测试解析完整 S3 URI"""
|
|
115
|
+
mock_client = MagicMock()
|
|
116
|
+
mock_client.put_object = MagicMock()
|
|
117
|
+
|
|
118
|
+
connect = S3Connect(client=mock_client)
|
|
119
|
+
result = connect._resolve_s3_uri("s3://bucket/key")
|
|
120
|
+
|
|
121
|
+
assert result.scheme == "s3"
|
|
122
|
+
assert result.netloc == "bucket"
|
|
123
|
+
assert result.path == "/key"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_resolve_s3_uri_without_scheme():
|
|
127
|
+
"""测试解析不带 scheme 的 URI"""
|
|
128
|
+
mock_client = MagicMock()
|
|
129
|
+
mock_client.put_object = MagicMock()
|
|
130
|
+
|
|
131
|
+
connect = S3Connect(client=mock_client)
|
|
132
|
+
result = connect._resolve_s3_uri("bucket/key")
|
|
133
|
+
|
|
134
|
+
assert result.scheme == "s3"
|
|
135
|
+
assert result.netloc == "bucket"
|
|
136
|
+
assert result.path == "/key"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_resolve_s3_uri_nested_path():
|
|
140
|
+
"""测试解析嵌套路径"""
|
|
141
|
+
mock_client = MagicMock()
|
|
142
|
+
mock_client.put_object = MagicMock()
|
|
143
|
+
|
|
144
|
+
connect = S3Connect(client=mock_client)
|
|
145
|
+
result = connect._resolve_s3_uri("s3://bucket/path/to/key")
|
|
146
|
+
|
|
147
|
+
assert result.netloc == "bucket"
|
|
148
|
+
assert result.path == "/path/to/key"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_resolve_s3_uri_with_http_prefix():
|
|
152
|
+
"""测试带 http 前缀的输入(会被转换)"""
|
|
153
|
+
mock_client = MagicMock()
|
|
154
|
+
mock_client.put_object = MagicMock()
|
|
155
|
+
|
|
156
|
+
connect = S3Connect(client=mock_client)
|
|
157
|
+
# http://bucket/key becomes s3://http://bucket/key, which is valid
|
|
158
|
+
result = connect._resolve_s3_uri("http://bucket/key")
|
|
159
|
+
assert result.scheme == "s3"
|
|
160
|
+
# The netloc becomes "http:" and path becomes "//bucket/key"
|
|
161
|
+
assert result.netloc == "http:"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_resolve_s3_uri_missing_bucket():
|
|
165
|
+
"""测试缺少 bucket 的 URI"""
|
|
166
|
+
mock_client = MagicMock()
|
|
167
|
+
mock_client.put_object = MagicMock()
|
|
168
|
+
|
|
169
|
+
connect = S3Connect(client=mock_client)
|
|
170
|
+
with pytest.raises(ValueError) as excinfo:
|
|
171
|
+
connect._resolve_s3_uri("s3:///key")
|
|
172
|
+
assert "missing bucket" in str(excinfo.value)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_resolve_s3_uri_missing_key():
|
|
176
|
+
"""测试缺少 key 的 URI"""
|
|
177
|
+
mock_client = MagicMock()
|
|
178
|
+
mock_client.put_object = MagicMock()
|
|
179
|
+
|
|
180
|
+
connect = S3Connect(client=mock_client)
|
|
181
|
+
with pytest.raises(ValueError) as excinfo:
|
|
182
|
+
connect._resolve_s3_uri("s3://bucket")
|
|
183
|
+
assert "missing key" in str(excinfo.value)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# =============================================================================
|
|
187
|
+
# Bucket Resolution Tests
|
|
188
|
+
# =============================================================================
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_resolve_bucket_valid():
|
|
192
|
+
"""测试解析有效的 bucket"""
|
|
193
|
+
mock_client = MagicMock()
|
|
194
|
+
mock_client.put_object = MagicMock()
|
|
195
|
+
|
|
196
|
+
connect = S3Connect(client=mock_client)
|
|
197
|
+
bucket = connect._resolve_bucket("s3://my-bucket/my-key")
|
|
198
|
+
|
|
199
|
+
assert bucket == "my-bucket"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_resolve_bucket_with_nested_path():
|
|
203
|
+
"""测试解析带嵌套路径的 bucket"""
|
|
204
|
+
mock_client = MagicMock()
|
|
205
|
+
mock_client.put_object = MagicMock()
|
|
206
|
+
|
|
207
|
+
connect = S3Connect(client=mock_client)
|
|
208
|
+
bucket = connect._resolve_bucket("s3://my-bucket/path/to/key")
|
|
209
|
+
|
|
210
|
+
assert bucket == "my-bucket"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# Key Resolution Tests
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_resolve_key_valid():
|
|
219
|
+
"""测试解析有效的 key"""
|
|
220
|
+
mock_client = MagicMock()
|
|
221
|
+
mock_client.put_object = MagicMock()
|
|
222
|
+
|
|
223
|
+
connect = S3Connect(client=mock_client)
|
|
224
|
+
key = connect._resolve_key("s3://bucket/my-key")
|
|
225
|
+
|
|
226
|
+
assert key == "my-key"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_resolve_key_with_nested_path():
|
|
230
|
+
"""测试解析嵌套路径的 key"""
|
|
231
|
+
mock_client = MagicMock()
|
|
232
|
+
mock_client.put_object = MagicMock()
|
|
233
|
+
|
|
234
|
+
connect = S3Connect(client=mock_client)
|
|
235
|
+
key = connect._resolve_key("s3://bucket/path/to/key.txt")
|
|
236
|
+
|
|
237
|
+
assert key == "path/to/key.txt"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_resolve_key_strips_leading_slash():
|
|
241
|
+
"""测试移除 key 开头的斜杠"""
|
|
242
|
+
mock_client = MagicMock()
|
|
243
|
+
mock_client.put_object = MagicMock()
|
|
244
|
+
|
|
245
|
+
connect = S3Connect(client=mock_client)
|
|
246
|
+
key = connect._resolve_key("s3://bucket/key")
|
|
247
|
+
|
|
248
|
+
# Path has leading slash, should be stripped
|
|
249
|
+
assert not key.startswith("/")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# =============================================================================
|
|
253
|
+
# put_bytes Tests
|
|
254
|
+
# =============================================================================
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_put_bytes_success():
|
|
258
|
+
"""测试成功写入字节"""
|
|
259
|
+
mock_client = MagicMock()
|
|
260
|
+
mock_client.put_object = MagicMock()
|
|
261
|
+
|
|
262
|
+
connect = S3Connect(client=mock_client)
|
|
263
|
+
connect.put_bytes(b"test data", "s3://bucket/key")
|
|
264
|
+
|
|
265
|
+
mock_client.put_object.assert_called_once()
|
|
266
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
267
|
+
assert call_kwargs["Bucket"] == "bucket"
|
|
268
|
+
assert call_kwargs["Key"] == "key"
|
|
269
|
+
assert call_kwargs["Body"] == b"test data"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_put_bytes_with_kwargs():
|
|
273
|
+
"""测试传递额外参数"""
|
|
274
|
+
mock_client = MagicMock()
|
|
275
|
+
mock_client.put_object = MagicMock()
|
|
276
|
+
|
|
277
|
+
connect = S3Connect(client=mock_client)
|
|
278
|
+
connect.put_bytes(b"test data", "s3://bucket/key", ContentType="application/json", ACL="public-read")
|
|
279
|
+
|
|
280
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
281
|
+
assert call_kwargs["ContentType"] == "application/json"
|
|
282
|
+
assert call_kwargs["ACL"] == "public-read"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_put_bytes_nested_path():
|
|
286
|
+
"""测试写入嵌套路径"""
|
|
287
|
+
mock_client = MagicMock()
|
|
288
|
+
mock_client.put_object = MagicMock()
|
|
289
|
+
|
|
290
|
+
connect = S3Connect(client=mock_client)
|
|
291
|
+
connect.put_bytes(b"data", "s3://bucket/path/to/file.txt")
|
|
292
|
+
|
|
293
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
294
|
+
assert call_kwargs["Key"] == "path/to/file.txt"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# =============================================================================
|
|
298
|
+
# get_bytes Tests
|
|
299
|
+
# =============================================================================
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_get_bytes_success():
|
|
303
|
+
"""测试成功读取字节"""
|
|
304
|
+
mock_client = MagicMock()
|
|
305
|
+
mock_response = {"Body": MagicMock()}
|
|
306
|
+
mock_response["Body"].read.return_value = b"test data"
|
|
307
|
+
mock_client.get_object.return_value = mock_response
|
|
308
|
+
|
|
309
|
+
connect = S3Connect(client=mock_client)
|
|
310
|
+
data = connect.get_bytes("s3://bucket/key")
|
|
311
|
+
|
|
312
|
+
assert data == b"test data"
|
|
313
|
+
mock_client.get_object.assert_called_once()
|
|
314
|
+
call_kwargs = mock_client.get_object.call_args[1]
|
|
315
|
+
assert call_kwargs["Bucket"] == "bucket"
|
|
316
|
+
assert call_kwargs["Key"] == "key"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_get_bytes_with_kwargs():
|
|
320
|
+
"""测试传递额外参数"""
|
|
321
|
+
mock_client = MagicMock()
|
|
322
|
+
mock_response = {"Body": MagicMock()}
|
|
323
|
+
mock_response["Body"].read.return_value = b"data"
|
|
324
|
+
mock_client.get_object.return_value = mock_response
|
|
325
|
+
|
|
326
|
+
connect = S3Connect(client=mock_client)
|
|
327
|
+
connect.get_bytes("s3://bucket/key", VersionId="version123")
|
|
328
|
+
|
|
329
|
+
call_kwargs = mock_client.get_object.call_args[1]
|
|
330
|
+
assert call_kwargs["VersionId"] == "version123"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_get_bytes_nested_path():
|
|
334
|
+
"""测试读取嵌套路径"""
|
|
335
|
+
mock_client = MagicMock()
|
|
336
|
+
mock_response = {"Body": MagicMock()}
|
|
337
|
+
mock_response["Body"].read.return_value = b"data"
|
|
338
|
+
mock_client.get_object.return_value = mock_response
|
|
339
|
+
|
|
340
|
+
connect = S3Connect(client=mock_client)
|
|
341
|
+
connect.get_bytes("s3://bucket/path/to/file.txt")
|
|
342
|
+
|
|
343
|
+
call_kwargs = mock_client.get_object.call_args[1]
|
|
344
|
+
assert call_kwargs["Key"] == "path/to/file.txt"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# =============================================================================
|
|
348
|
+
# put_object Tests
|
|
349
|
+
# =============================================================================
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_put_object_returns_connectref():
|
|
353
|
+
"""测试 put_object 返回 ConnectRef"""
|
|
354
|
+
mock_client = MagicMock()
|
|
355
|
+
mock_client.put_object = MagicMock()
|
|
356
|
+
|
|
357
|
+
connect = S3Connect(client=mock_client)
|
|
358
|
+
ref = connect.put_object(b"data", "s3://bucket/key")
|
|
359
|
+
|
|
360
|
+
assert isinstance(ref, ConnectRef)
|
|
361
|
+
assert ref.location == "s3://bucket/key"
|
|
362
|
+
assert ref.kind == "s3"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_put_object_calls_put_bytes():
|
|
366
|
+
"""测试 put_object 调用 put_bytes"""
|
|
367
|
+
mock_client = MagicMock()
|
|
368
|
+
mock_client.put_object = MagicMock()
|
|
369
|
+
|
|
370
|
+
connect = S3Connect(client=mock_client)
|
|
371
|
+
connect.put_object(b"test", "s3://bucket/key")
|
|
372
|
+
|
|
373
|
+
mock_client.put_object.assert_called_once()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_put_object_with_kwargs():
|
|
377
|
+
"""测试 put_object 传递 kwargs"""
|
|
378
|
+
mock_client = MagicMock()
|
|
379
|
+
mock_client.put_object = MagicMock()
|
|
380
|
+
|
|
381
|
+
connect = S3Connect(client=mock_client)
|
|
382
|
+
connect.put_object(b"data", "s3://bucket/key", ContentType="text/plain")
|
|
383
|
+
|
|
384
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
385
|
+
assert call_kwargs["ContentType"] == "text/plain"
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# =============================================================================
|
|
389
|
+
# get_object Tests
|
|
390
|
+
# =============================================================================
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_get_object_returns_bytes():
|
|
394
|
+
"""测试 get_object 返回字节"""
|
|
395
|
+
mock_client = MagicMock()
|
|
396
|
+
mock_response = {"Body": MagicMock()}
|
|
397
|
+
mock_response["Body"].read.return_value = b"test data"
|
|
398
|
+
mock_client.get_object.return_value = mock_response
|
|
399
|
+
|
|
400
|
+
connect = S3Connect(client=mock_client)
|
|
401
|
+
data = connect.get_object("s3://bucket/key")
|
|
402
|
+
|
|
403
|
+
assert isinstance(data, bytes)
|
|
404
|
+
assert data == b"test data"
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def test_get_object_calls_get_bytes():
|
|
408
|
+
"""测试 get_object 调用 get_bytes"""
|
|
409
|
+
mock_client = MagicMock()
|
|
410
|
+
mock_response = {"Body": MagicMock()}
|
|
411
|
+
mock_response["Body"].read.return_value = b"data"
|
|
412
|
+
mock_client.get_object.return_value = mock_response
|
|
413
|
+
|
|
414
|
+
connect = S3Connect(client=mock_client)
|
|
415
|
+
connect.get_object("s3://bucket/key")
|
|
416
|
+
|
|
417
|
+
mock_client.get_object.assert_called_once()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_get_object_with_kwargs():
|
|
421
|
+
"""测试 get_object 传递 kwargs"""
|
|
422
|
+
mock_client = MagicMock()
|
|
423
|
+
mock_response = {"Body": MagicMock()}
|
|
424
|
+
mock_response["Body"].read.return_value = b"data"
|
|
425
|
+
mock_client.get_object.return_value = mock_response
|
|
426
|
+
|
|
427
|
+
connect = S3Connect(client=mock_client)
|
|
428
|
+
connect.get_object("s3://bucket/key", Range="bytes=0-100")
|
|
429
|
+
|
|
430
|
+
call_kwargs = mock_client.get_object.call_args[1]
|
|
431
|
+
assert call_kwargs["Range"] == "bytes=0-100"
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# =============================================================================
|
|
435
|
+
# Integration Tests
|
|
436
|
+
# =============================================================================
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_round_trip():
|
|
440
|
+
"""测试完整的读写流程"""
|
|
441
|
+
mock_client = MagicMock()
|
|
442
|
+
mock_client.put_object = MagicMock()
|
|
443
|
+
mock_response = {"Body": MagicMock()}
|
|
444
|
+
mock_response["Body"].read.return_value = b"test data"
|
|
445
|
+
mock_client.get_object.return_value = mock_response
|
|
446
|
+
|
|
447
|
+
connect = S3Connect(client=mock_client)
|
|
448
|
+
|
|
449
|
+
# Write
|
|
450
|
+
ref = connect.put_object(b"test data", "s3://bucket/key")
|
|
451
|
+
assert ref.location == "s3://bucket/key"
|
|
452
|
+
|
|
453
|
+
# Read
|
|
454
|
+
data = connect.get_object("s3://bucket/key")
|
|
455
|
+
assert data == b"test data"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def test_multiple_operations():
|
|
459
|
+
"""测试多次操作"""
|
|
460
|
+
mock_client = MagicMock()
|
|
461
|
+
mock_client.put_object = MagicMock()
|
|
462
|
+
mock_response = {"Body": MagicMock()}
|
|
463
|
+
mock_response["Body"].read.return_value = b"data"
|
|
464
|
+
mock_client.get_object.return_value = mock_response
|
|
465
|
+
|
|
466
|
+
connect = S3Connect(client=mock_client)
|
|
467
|
+
|
|
468
|
+
# Multiple writes
|
|
469
|
+
connect.put_object(b"data1", "s3://bucket/key1")
|
|
470
|
+
connect.put_object(b"data2", "s3://bucket/key2")
|
|
471
|
+
|
|
472
|
+
assert mock_client.put_object.call_count == 2
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def test_different_buckets():
|
|
476
|
+
"""测试不同的 bucket"""
|
|
477
|
+
mock_client = MagicMock()
|
|
478
|
+
mock_client.put_object = MagicMock()
|
|
479
|
+
|
|
480
|
+
connect = S3Connect(client=mock_client)
|
|
481
|
+
|
|
482
|
+
connect.put_object(b"data", "s3://bucket1/key")
|
|
483
|
+
connect.put_object(b"data", "s3://bucket2/key")
|
|
484
|
+
|
|
485
|
+
calls = mock_client.put_object.call_args_list
|
|
486
|
+
assert calls[0][1]["Bucket"] == "bucket1"
|
|
487
|
+
assert calls[1][1]["Bucket"] == "bucket2"
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# =============================================================================
|
|
491
|
+
# Edge Cases
|
|
492
|
+
# =============================================================================
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_empty_data():
|
|
496
|
+
"""测试空数据"""
|
|
497
|
+
mock_client = MagicMock()
|
|
498
|
+
mock_client.put_object = MagicMock()
|
|
499
|
+
|
|
500
|
+
connect = S3Connect(client=mock_client)
|
|
501
|
+
connect.put_object(b"", "s3://bucket/key")
|
|
502
|
+
|
|
503
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
504
|
+
assert call_kwargs["Body"] == b""
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_large_data():
|
|
508
|
+
"""测试大数据"""
|
|
509
|
+
mock_client = MagicMock()
|
|
510
|
+
mock_client.put_object = MagicMock()
|
|
511
|
+
|
|
512
|
+
large_data = b"x" * (10 * 1024 * 1024) # 10MB
|
|
513
|
+
|
|
514
|
+
connect = S3Connect(client=mock_client)
|
|
515
|
+
connect.put_object(large_data, "s3://bucket/key")
|
|
516
|
+
|
|
517
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
518
|
+
assert len(call_kwargs["Body"]) == 10 * 1024 * 1024
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_special_characters_in_key():
|
|
522
|
+
"""测试 key 中的特殊字符"""
|
|
523
|
+
mock_client = MagicMock()
|
|
524
|
+
mock_client.put_object = MagicMock()
|
|
525
|
+
|
|
526
|
+
connect = S3Connect(client=mock_client)
|
|
527
|
+
connect.put_object(b"data", "s3://bucket/path with spaces/file.txt")
|
|
528
|
+
|
|
529
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
530
|
+
assert "path with spaces" in call_kwargs["Key"]
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def test_unicode_in_key():
|
|
534
|
+
"""测试 key 中的 Unicode 字符"""
|
|
535
|
+
mock_client = MagicMock()
|
|
536
|
+
mock_client.put_object = MagicMock()
|
|
537
|
+
|
|
538
|
+
connect = S3Connect(client=mock_client)
|
|
539
|
+
connect.put_object(b"data", "s3://bucket/文件/数据.txt")
|
|
540
|
+
|
|
541
|
+
call_kwargs = mock_client.put_object.call_args[1]
|
|
542
|
+
assert "文件" in call_kwargs["Key"]
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# =============================================================================
|
|
546
|
+
# Error Handling Tests
|
|
547
|
+
# =============================================================================
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def test_put_bytes_client_error():
|
|
551
|
+
"""测试客户端错误"""
|
|
552
|
+
mock_client = MagicMock()
|
|
553
|
+
mock_client.put_object.side_effect = Exception("S3 Error")
|
|
554
|
+
|
|
555
|
+
connect = S3Connect(client=mock_client)
|
|
556
|
+
|
|
557
|
+
with pytest.raises(Exception) as excinfo:
|
|
558
|
+
connect.put_object(b"data", "s3://bucket/key")
|
|
559
|
+
assert "S3 Error" in str(excinfo.value)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def test_get_bytes_client_error():
|
|
563
|
+
"""测试读取时客户端错误"""
|
|
564
|
+
mock_client = MagicMock()
|
|
565
|
+
mock_client.get_object.side_effect = Exception("Not Found")
|
|
566
|
+
|
|
567
|
+
connect = S3Connect(client=mock_client)
|
|
568
|
+
|
|
569
|
+
with pytest.raises(Exception) as excinfo:
|
|
570
|
+
connect.get_object("s3://bucket/nonexistent")
|
|
571
|
+
assert "Not Found" in str(excinfo.value)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# =============================================================================
|
|
575
|
+
# Type Tests
|
|
576
|
+
# =============================================================================
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def test_returns_correct_types():
|
|
580
|
+
"""测试返回正确的类型"""
|
|
581
|
+
mock_client = MagicMock()
|
|
582
|
+
mock_client.put_object = MagicMock()
|
|
583
|
+
mock_response = {"Body": MagicMock()}
|
|
584
|
+
mock_response["Body"].read.return_value = b"data"
|
|
585
|
+
mock_client.get_object.return_value = mock_response
|
|
586
|
+
|
|
587
|
+
connect = S3Connect(client=mock_client)
|
|
588
|
+
|
|
589
|
+
# put_object returns ConnectRef
|
|
590
|
+
ref = connect.put_object(b"data", "s3://bucket/key")
|
|
591
|
+
assert isinstance(ref, ConnectRef)
|
|
592
|
+
|
|
593
|
+
# get_object returns bytes
|
|
594
|
+
data = connect.get_object("s3://bucket/key")
|
|
595
|
+
assert isinstance(data, bytes)
|
|
596
|
+
|
|
597
|
+
# put_bytes returns None
|
|
598
|
+
result = connect.put_bytes(b"data", "s3://bucket/key2")
|
|
599
|
+
assert result is None
|
|
600
|
+
|
|
601
|
+
# get_bytes returns bytes
|
|
602
|
+
data = connect.get_bytes("s3://bucket/key")
|
|
603
|
+
assert isinstance(data, bytes)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data module for xfintech.
|
|
3
|
+
|
|
4
|
+
This module provides the core data acquisition and processing infrastructure:
|
|
5
|
+
|
|
6
|
+
Submodules:
|
|
7
|
+
- common: Common utilities (Cache, Retry, Metric, Params, Paginate, Coolant)
|
|
8
|
+
- job: Job system with registry and error handling
|
|
9
|
+
- relay: Relay client for remote API access
|
|
10
|
+
- source: Data source implementations (tushare, baostock)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from . import common, job, relay, source
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"common",
|
|
17
|
+
"job",
|
|
18
|
+
"relay",
|
|
19
|
+
"source",
|
|
20
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .cache import Cache
|
|
2
|
+
from .coolant import Coolant
|
|
3
|
+
from .metric import Metric
|
|
4
|
+
from .paginate import Paginate
|
|
5
|
+
from .params import Params
|
|
6
|
+
from .retry import Retry
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Cache",
|
|
10
|
+
"Metric",
|
|
11
|
+
"Params",
|
|
12
|
+
"Coolant",
|
|
13
|
+
"Paginate",
|
|
14
|
+
"Retry",
|
|
15
|
+
]
|