cfquant 0.2.68__tar.gz → 0.2.71__tar.gz
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.
- {cfquant-0.2.68 → cfquant-0.2.71}/PKG-INFO +1 -1
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_python_runtime.py +181 -54
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_deploy.py +11 -3
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_python_runtime_browser.py +73 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_integration.py +32 -0
- cfquant-0.2.71/cfquant/tests/test_qmt_python_mirror.py +181 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_python_runtime.py +139 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_deploy.py +19 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/version.py +2 -2
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/SOURCES.txt +1 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant_web_server.py +163 -40
- {cfquant-0.2.68 → cfquant-0.2.71}/pyproject.toml +1 -1
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SH.py +1 -1
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SZ.py +1 -1
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/app.js +66 -44
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/index.html +2 -2
- {cfquant-0.2.68 → cfquant-0.2.71}/LICENSE +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/__init__.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/MANIFEST.in +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/README.md +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/__init__.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/algo.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/build_info.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/channels.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/cli.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/client.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/conditional_orders.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/config.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/diagnostic_context.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/hub.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/level2.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/log_management.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/management.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/monitor_notifications.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/monitor_performance.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/notify.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/planning_broker.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/position_sizing.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/protocol.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_component_login.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_inner.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_inner_catalog.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_inner_runtime.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_mac_agent.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_native_files.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_native_worker.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_agent.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_agent.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_discovery.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_runtime.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_settings.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/qmt_vm_worker.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/smart_orders.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_bridge.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_client.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_manager.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/subscription_store.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/10_/345/221/275/344/273/244/350/241/214/346/216/245/345/217/243/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/11_/346/231/256/351/200/232/346/241/245/346/216/245/346/264/276/345/217/221/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/13_/347/256/241/351/201/223/344/270/255/345/277/203/350/277/236/346/216/245/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/14_/350/277/220/350/241/214/346/227/266/346/212/245/345/221/212/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/15_/347/275/221/351/241/265/347/212/266/346/200/201/347/274/223/345/255/230/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/17_/345/244/247QMT/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/18_/351/253/230/347/272/247/346/250/241/345/274/217/345/256/236/346/234/272/350/201/224/350/260/203.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/19_/345/256/236/346/234/272/350/201/224/350/260/203/345/210/244/345/256/232/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/20_/350/201/224/350/260/203/351/227/256/351/242/230/344/277/256/345/244/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/21_/345/215/225/350/202/241/350/241/214/346/203/205/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/24_/350/264/246/346/210/267/347/274/223/345/255/230/346/214/211/351/234/200/345/210/267/346/226/260/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/27_/345/205/250/346/216/250/350/256/242/351/230/205/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/30_cftrader/346/211/271/351/207/217/344/270/213/345/215/225/346/200/247/350/203/275/345/237/272/345/207/206.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/31_cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/33_/345/220/214/346/255/245/344/270/213/345/215/225/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/4_/344/272/244/346/230/223/345/247/224/346/211/230/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/7_/345/220/214/346/255/245/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/8_/346/210/220/344/272/244/350/256/242/345/215/225/345/222/214/345/247/224/346/211/230/350/256/242/345/215/225/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_activity_log_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_auto_login_app.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_vm_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_binding_webhook_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_diagnostic_context.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_history_download_memory.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_monitor_notifications.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_monitor_performance.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_navigation_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_notify.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_reference.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_web.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_files.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_reuse.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_worker.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_package_import.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_native.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_settings.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_smart_orders.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_api.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_docs.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_split_order_identity.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_strategy_recovery_api.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_subscription_persistence.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_version_check.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tests/test_version_check_browser.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/THIRD_PARTY.md +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/COPYING +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/LICENSE +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/NEWS +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/README +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/THANKS.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.html +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx.1 +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tools/upx/upx.exe +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tx.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/webhook.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant/xttype.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/readme.md" +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/requirements.txt +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/setup.cfg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/activity-log.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/cfquant-architecture.html +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/navigation.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/notify-api-data.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/qmt-inner-api-data.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/smart-order-api-data.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.68 → cfquant-0.2.71}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -15,7 +15,9 @@ from __future__ import annotations
|
|
|
15
15
|
from dataclasses import dataclass, asdict
|
|
16
16
|
import configparser
|
|
17
17
|
import hashlib
|
|
18
|
+
import http.client
|
|
18
19
|
import json
|
|
20
|
+
import logging
|
|
19
21
|
import os
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
import re
|
|
@@ -26,6 +28,7 @@ import time
|
|
|
26
28
|
from typing import Dict, Optional
|
|
27
29
|
import urllib.error
|
|
28
30
|
import urllib.request
|
|
31
|
+
from urllib.parse import urlencode
|
|
29
32
|
import zipfile
|
|
30
33
|
|
|
31
34
|
|
|
@@ -33,6 +36,16 @@ class QmtPythonRuntimeError(RuntimeError):
|
|
|
33
36
|
"""A download, validation, activation, or restart operation failed."""
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
class _DownloadSourceError(QmtPythonRuntimeError):
|
|
40
|
+
"""A remote failure that permits trying another copy of the same package."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _NoMirrorRedirect(urllib.request.HTTPRedirectHandler):
|
|
44
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
45
|
+
# Never forward the shared mirror key to a redirected host.
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
36
49
|
@dataclass(frozen=True)
|
|
37
50
|
class QmtPythonPackage:
|
|
38
51
|
runtime: str
|
|
@@ -41,6 +54,8 @@ class QmtPythonPackage:
|
|
|
41
54
|
filename: str
|
|
42
55
|
md5: str
|
|
43
56
|
force_update: bool = False
|
|
57
|
+
sha256: str = ""
|
|
58
|
+
size: int = 0
|
|
44
59
|
|
|
45
60
|
def as_dict(self):
|
|
46
61
|
return asdict(self)
|
|
@@ -67,6 +82,10 @@ _RUNTIME_ALIASES = {"36": "py36", "3.6": "py36", "python36": "py36",
|
|
|
67
82
|
_PY312_MIN_QMT_VERSION = (2, 1, 33, 0)
|
|
68
83
|
_CONFIG_LOCKS: Dict[str, threading.RLock] = {}
|
|
69
84
|
_CONFIG_LOCKS_GUARD = threading.Lock()
|
|
85
|
+
_MIRROR_BASE = "https://cfquant.org/api/qmt/python-runtime/"
|
|
86
|
+
_MIRROR_KEY_HEADER = "X-CFQuant-Runtime-Key"
|
|
87
|
+
_MIRROR_KEY = "cfq-runtime-v1-4859bf5af16942e38b30f128bd540714"
|
|
88
|
+
_LOG = logging.getLogger(__name__)
|
|
70
89
|
|
|
71
90
|
|
|
72
91
|
def _canonical_runtime(runtime: str) -> str:
|
|
@@ -118,6 +137,10 @@ class QmtPythonRuntime:
|
|
|
118
137
|
self.staging_dir = self.state_dir / "staging"
|
|
119
138
|
self.backup_dir = self.state_dir / "backups"
|
|
120
139
|
self._opener = opener or urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
140
|
+
self._mirror_opener = opener or urllib.request.build_opener(
|
|
141
|
+
urllib.request.ProxyHandler({}), _NoMirrorRedirect())
|
|
142
|
+
self._mirror_cache = None
|
|
143
|
+
self._mirror_checked_at = float("-inf")
|
|
121
144
|
self._lock = self._lock_for(str(self.bin_dir))
|
|
122
145
|
|
|
123
146
|
@staticmethod
|
|
@@ -131,10 +154,50 @@ class QmtPythonRuntime:
|
|
|
131
154
|
|
|
132
155
|
def _request(self, url, *, headers=None, timeout=None):
|
|
133
156
|
request = urllib.request.Request(url, headers=headers or {}, method="GET")
|
|
134
|
-
|
|
157
|
+
opener = self._mirror_opener if url.startswith(_MIRROR_BASE) else self._opener
|
|
158
|
+
return opener.open(request, timeout=timeout or self.timeout)
|
|
159
|
+
|
|
160
|
+
def _mirror_status(self):
|
|
161
|
+
if time.monotonic() - self._mirror_checked_at < 5:
|
|
162
|
+
return self._mirror_cache
|
|
163
|
+
self._mirror_checked_at = time.monotonic()
|
|
164
|
+
self._mirror_cache = None
|
|
165
|
+
try:
|
|
166
|
+
with self._request(_MIRROR_BASE + "status", headers={_MIRROR_KEY_HEADER: _MIRROR_KEY},
|
|
167
|
+
timeout=min(self.timeout, 5)) as response:
|
|
168
|
+
data = json.loads(response.read(65537).decode("utf-8"))
|
|
169
|
+
if (not data.get("ok") or data.get("schema") != 1 or not isinstance(data.get("packages"), dict)
|
|
170
|
+
or not isinstance(data.get("busy"), bool)):
|
|
171
|
+
raise ValueError("invalid mirror status")
|
|
172
|
+
self._mirror_cache = data
|
|
173
|
+
except (OSError, ValueError, AttributeError, http.client.HTTPException) as exc:
|
|
174
|
+
_LOG.info("QMT runtime mirror status unavailable: %s", exc)
|
|
175
|
+
return self._mirror_cache
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _package_from_mirror(data, runtime):
|
|
179
|
+
try:
|
|
180
|
+
item = data["packages"][runtime]
|
|
181
|
+
if (not re.fullmatch(r"[A-Za-z0-9_.-]+\.zip", item["filename"])
|
|
182
|
+
or not re.fullmatch(r"[0-9a-f]{32}", item["md5"])
|
|
183
|
+
or not re.fullmatch(r"[0-9a-f]{64}", item["sha256"])
|
|
184
|
+
or not item["url"].startswith(("http://", "https://"))
|
|
185
|
+
or int(item["size"]) <= 0):
|
|
186
|
+
return None
|
|
187
|
+
return QmtPythonPackage(runtime, item["version"], item["url"], item["filename"],
|
|
188
|
+
item["md5"], bool(item.get("force_update")),
|
|
189
|
+
item["sha256"], int(item["size"]))
|
|
190
|
+
except (KeyError, TypeError, ValueError, AttributeError):
|
|
191
|
+
return None
|
|
135
192
|
|
|
136
193
|
def fetch_package(self, runtime="py312") -> QmtPythonPackage:
|
|
137
194
|
runtime = _canonical_runtime(runtime)
|
|
195
|
+
mirrored = self._package_from_mirror(self._mirror_status(), runtime)
|
|
196
|
+
if mirrored:
|
|
197
|
+
return mirrored
|
|
198
|
+
return self._fetch_upstream_package(runtime)
|
|
199
|
+
|
|
200
|
+
def _fetch_upstream_package(self, runtime):
|
|
138
201
|
cfg_url = "http://download.thinktrader.net:8800/download/" + _CONFIG_NAMES[runtime]
|
|
139
202
|
try:
|
|
140
203
|
with self._request(cfg_url) as response:
|
|
@@ -350,68 +413,127 @@ class QmtPythonRuntime:
|
|
|
350
413
|
destination = self.download_dir / package.filename
|
|
351
414
|
partial = destination.with_suffix(destination.suffix + ".part")
|
|
352
415
|
with self._lock:
|
|
353
|
-
if destination.is_file() and self.
|
|
416
|
+
if destination.is_file() and self._archive_matches(destination, package):
|
|
354
417
|
_callback(progress_callback, QmtPythonProgress(
|
|
355
418
|
runtime, "download_verified", destination.stat().st_size,
|
|
356
419
|
destination.stat().st_size, 100, 0, 0, "download already verified"))
|
|
357
420
|
return destination
|
|
358
|
-
|
|
359
|
-
|
|
421
|
+
identity_file = partial.with_suffix(partial.suffix + ".identity")
|
|
422
|
+
identity = package.md5 + ":" + package.sha256
|
|
423
|
+
previous = identity_file.read_text("ascii") if identity_file.exists() else ""
|
|
424
|
+
if partial.exists() and (not keep_existing or previous != identity):
|
|
360
425
|
partial.unlink()
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
426
|
+
identity_file.write_text(identity, encoding="ascii")
|
|
427
|
+
self._check_cancel(cancel_event)
|
|
428
|
+
snapshot = self._mirror_status()
|
|
429
|
+
mirrored = self._package_from_mirror(snapshot, runtime)
|
|
430
|
+
mirror_url = _MIRROR_BASE + "download?" + urlencode(dict(runtime=runtime, md5=package.md5))
|
|
431
|
+
sources = []
|
|
432
|
+
if mirrored and mirrored.md5 == package.md5 and not snapshot["busy"]:
|
|
433
|
+
sources.append(("官网", mirror_url))
|
|
434
|
+
sources.extend([("讯投", package.url), ("官网(失败回退)", mirror_url + "&force=1")])
|
|
435
|
+
errors = []
|
|
436
|
+
for source, url in sources:
|
|
437
|
+
self._check_cancel(cancel_event)
|
|
438
|
+
_LOG.info("QMT runtime download source=%s runtime=%s file=%s", source, runtime, package.filename)
|
|
439
|
+
_callback(progress_callback, QmtPythonProgress(runtime, "download", message="正在通过%s下载运行库" % source))
|
|
440
|
+
try:
|
|
441
|
+
self._download_source(package, url, partial, source, progress_callback, cancel_event)
|
|
442
|
+
break
|
|
443
|
+
except _DownloadSourceError as exc:
|
|
444
|
+
errors.append("%s: %s" % (source, exc))
|
|
445
|
+
_LOG.warning("QMT runtime source failed source=%s runtime=%s error=%s", source, runtime, exc)
|
|
373
446
|
else:
|
|
374
|
-
|
|
375
|
-
raise QmtPythonRuntimeError("unexpected QMT download HTTP status: %s" % status)
|
|
376
|
-
total_header = response.headers.get("Content-Range", "").split("/")[-1]
|
|
377
|
-
total = int(total_header) if total_header.isdigit() else int(response.headers.get("Content-Length") or 0) + done
|
|
378
|
-
started = time.monotonic()
|
|
379
|
-
_callback(progress_callback, QmtPythonProgress(runtime, "download", done, total,
|
|
380
|
-
(done / total * 100) if total else 0,
|
|
381
|
-
0, None, "downloading"))
|
|
382
|
-
try:
|
|
383
|
-
with response, open(partial, mode) as stream:
|
|
384
|
-
while True:
|
|
385
|
-
if cancel_event is not None and cancel_event.is_set():
|
|
386
|
-
raise QmtPythonRuntimeError("QMT runtime download cancelled")
|
|
387
|
-
chunk = response.read(1024 * 1024)
|
|
388
|
-
if not chunk:
|
|
389
|
-
break
|
|
390
|
-
stream.write(chunk)
|
|
391
|
-
done += len(chunk)
|
|
392
|
-
elapsed = max(time.monotonic() - started, 0.001)
|
|
393
|
-
speed = (done - (existing if mode == "ab" else 0)) / elapsed
|
|
394
|
-
eta = ((total - done) / speed) if speed > 0 and total else None
|
|
395
|
-
_callback(progress_callback, QmtPythonProgress(
|
|
396
|
-
runtime, "download", done, total,
|
|
397
|
-
(done / total * 100) if total else 0, speed, eta, "downloading"))
|
|
398
|
-
stream.flush()
|
|
399
|
-
os.fsync(stream.fileno())
|
|
400
|
-
except QmtPythonRuntimeError:
|
|
401
|
-
raise
|
|
402
|
-
except OSError as exc:
|
|
403
|
-
raise QmtPythonRuntimeError("cannot write QMT runtime download: %s" % exc) from exc
|
|
404
|
-
actual = self._md5(partial)
|
|
405
|
-
if actual != package.md5:
|
|
406
|
-
raise QmtPythonRuntimeError("QMT runtime MD5 mismatch: expected %s, got %s" %
|
|
407
|
-
(package.md5, actual))
|
|
447
|
+
raise QmtPythonRuntimeError("QMT runtime download failed; " + "; ".join(errors))
|
|
408
448
|
os.replace(partial, destination)
|
|
449
|
+
identity_file.unlink()
|
|
450
|
+
size = destination.stat().st_size
|
|
409
451
|
_callback(progress_callback, QmtPythonProgress(runtime, "download_verified",
|
|
410
|
-
|
|
411
|
-
total or destination.stat().st_size,
|
|
452
|
+
size, size,
|
|
412
453
|
100, 0, 0, "download verified"))
|
|
413
454
|
return destination
|
|
414
455
|
|
|
456
|
+
@staticmethod
|
|
457
|
+
def _check_cancel(event):
|
|
458
|
+
if event is not None and event.is_set():
|
|
459
|
+
raise QmtPythonRuntimeError("QMT runtime download cancelled")
|
|
460
|
+
|
|
461
|
+
def _archive_matches(self, path, package):
|
|
462
|
+
if package.size and path.stat().st_size != package.size:
|
|
463
|
+
return False
|
|
464
|
+
if self._md5(path) != package.md5:
|
|
465
|
+
return False
|
|
466
|
+
if package.sha256:
|
|
467
|
+
digest = hashlib.sha256()
|
|
468
|
+
with path.open("rb") as stream:
|
|
469
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
470
|
+
digest.update(chunk)
|
|
471
|
+
return digest.hexdigest() == package.sha256
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
def _download_source(self, package, url, partial, source, callback, cancel_event):
|
|
475
|
+
existing = partial.stat().st_size if partial.exists() else 0
|
|
476
|
+
if existing and self._archive_matches(partial, package):
|
|
477
|
+
return
|
|
478
|
+
if package.size and existing >= package.size:
|
|
479
|
+
partial.unlink()
|
|
480
|
+
existing = 0
|
|
481
|
+
headers = {"Accept-Encoding": "identity"}
|
|
482
|
+
if url.startswith(_MIRROR_BASE):
|
|
483
|
+
headers[_MIRROR_KEY_HEADER] = _MIRROR_KEY
|
|
484
|
+
if existing:
|
|
485
|
+
headers["Range"] = "bytes=%d-" % existing
|
|
486
|
+
try:
|
|
487
|
+
response = self._request(url, headers=headers)
|
|
488
|
+
except (OSError, http.client.HTTPException) as exc:
|
|
489
|
+
if isinstance(exc, urllib.error.HTTPError) and exc.code == 416:
|
|
490
|
+
partial.unlink(missing_ok=True)
|
|
491
|
+
raise _DownloadSourceError("cannot connect: %s" % exc) from exc
|
|
492
|
+
with response:
|
|
493
|
+
status = response.getcode()
|
|
494
|
+
if status == 206 and existing:
|
|
495
|
+
match = re.fullmatch(r"bytes (\d+)-(\d+)/(\d+)", response.headers.get("Content-Range", ""))
|
|
496
|
+
if not match or int(match[1]) != existing or int(match[2]) != int(match[3]) - 1:
|
|
497
|
+
raise _DownloadSourceError("invalid resume Content-Range")
|
|
498
|
+
mode, done, total = "ab", existing, int(match[3])
|
|
499
|
+
elif status == 200:
|
|
500
|
+
mode, done = "wb", 0
|
|
501
|
+
try:
|
|
502
|
+
total = int(response.headers.get("Content-Length") or package.size)
|
|
503
|
+
except ValueError as exc:
|
|
504
|
+
raise _DownloadSourceError("invalid Content-Length") from exc
|
|
505
|
+
else:
|
|
506
|
+
raise _DownloadSourceError("unexpected HTTP status: %s" % status)
|
|
507
|
+
if package.size and total and total != package.size:
|
|
508
|
+
raise _DownloadSourceError("package size differs from manifest")
|
|
509
|
+
started, initial = time.monotonic(), done
|
|
510
|
+
with partial.open(mode) as stream:
|
|
511
|
+
while True:
|
|
512
|
+
self._check_cancel(cancel_event)
|
|
513
|
+
try:
|
|
514
|
+
chunk = response.read(1024 * 1024)
|
|
515
|
+
except (OSError, http.client.HTTPException) as exc:
|
|
516
|
+
raise _DownloadSourceError("connection interrupted: %s" % exc) from exc
|
|
517
|
+
if not chunk:
|
|
518
|
+
break
|
|
519
|
+
stream.write(chunk) # Local disk failures must not trigger another download.
|
|
520
|
+
done += len(chunk)
|
|
521
|
+
if total and done > total:
|
|
522
|
+
break
|
|
523
|
+
speed = (done - initial) / max(time.monotonic() - started, 0.001)
|
|
524
|
+
_callback(callback, QmtPythonProgress(
|
|
525
|
+
package.runtime, "download", done, total, (done / total * 100) if total else 0,
|
|
526
|
+
speed, (total - done) / speed if total and speed else None,
|
|
527
|
+
"正在通过%s下载运行库" % source))
|
|
528
|
+
stream.flush()
|
|
529
|
+
os.fsync(stream.fileno())
|
|
530
|
+
self._check_cancel(cancel_event)
|
|
531
|
+
if total and done < total:
|
|
532
|
+
raise _DownloadSourceError("download truncated: %d/%d" % (done, total))
|
|
533
|
+
if not self._archive_matches(partial, package):
|
|
534
|
+
partial.unlink()
|
|
535
|
+
raise _DownloadSourceError("QMT runtime MD5/SHA-256 mismatch")
|
|
536
|
+
|
|
415
537
|
@staticmethod
|
|
416
538
|
def _safe_extract(archive, destination):
|
|
417
539
|
destination = Path(destination).resolve()
|
|
@@ -442,7 +564,7 @@ class QmtPythonRuntime:
|
|
|
442
564
|
runtime = _canonical_runtime(runtime)
|
|
443
565
|
package = package or self.fetch_package(runtime)
|
|
444
566
|
archive = self.download_dir / package.filename
|
|
445
|
-
if not archive.is_file() or self.
|
|
567
|
+
if not archive.is_file() or not self._archive_matches(archive, package):
|
|
446
568
|
raise QmtPythonRuntimeError("verified QMT runtime archive is missing")
|
|
447
569
|
with self._lock:
|
|
448
570
|
self.staging_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -655,7 +777,12 @@ class QmtPythonRuntime:
|
|
|
655
777
|
if (account_id or self.account_id) else None
|
|
656
778
|
)
|
|
657
779
|
finally:
|
|
658
|
-
|
|
780
|
+
# A managed Web binding can supply a bootstrap callback that starts
|
|
781
|
+
# QMT, waits for the first login to create Config.xml, and then
|
|
782
|
+
# performs the final runtime switch/restart. Keep the standalone
|
|
783
|
+
# API's old safe behavior (leave a brand-new, unconfigured QMT
|
|
784
|
+
# stopped) when no callback was supplied.
|
|
785
|
+
if restart and (not deferred_account_config or was_running or restart_callback):
|
|
659
786
|
_callback(progress_callback, QmtPythonProgress(runtime, "restarting", message="正在重新启动 QMT"))
|
|
660
787
|
if restart_callback:
|
|
661
788
|
restarted = restart_callback()
|
|
@@ -82,7 +82,7 @@ STATES = {
|
|
|
82
82
|
"waiting_start": "QMT 已启动,等待托管策略启动及通道连接",
|
|
83
83
|
"waiting_manual_start": "QMT 已启动,自启未开启,请在模型交易中运行托管策略",
|
|
84
84
|
"running": "托管策略已运行",
|
|
85
|
-
"waiting_account": "
|
|
85
|
+
"waiting_account": "等待 QMT 登录生成账号绑定键;首次登录后请正常退出 QMT,再重新启动以完成策略部署。仍无法识别时再补充模型账号 Key",
|
|
86
86
|
"configured": "模型已配置,等待下次启动 QMT 生效",
|
|
87
87
|
"disabled": "自动运行已关闭,托管策略已禁用",
|
|
88
88
|
"error": "策略部署失败",
|
|
@@ -915,15 +915,16 @@ class QmtStrategyManager:
|
|
|
915
915
|
self._save()
|
|
916
916
|
|
|
917
917
|
def prepare_for_start(self, directory):
|
|
918
|
-
"""Finish
|
|
918
|
+
"""Finish offline deployment, or allow login to supply a missing key."""
|
|
919
919
|
root = qmt_root(directory)
|
|
920
|
+
waiting_account = []
|
|
920
921
|
with self.lock:
|
|
921
922
|
if self.load_error:
|
|
922
923
|
raise ValueError(self.load_error)
|
|
923
924
|
jobs = [job for job in self.jobs.values()
|
|
924
925
|
if os.path.normcase(str(qmt_root(job["root"]))) == os.path.normcase(str(root))]
|
|
925
926
|
if not jobs:
|
|
926
|
-
return
|
|
927
|
+
return {"waiting_account": waiting_account}
|
|
927
928
|
if self.process_checker(root):
|
|
928
929
|
raise RuntimeError("QMT 尚未完全退出,无法完成启动前策略部署")
|
|
929
930
|
try:
|
|
@@ -957,6 +958,12 @@ class QmtStrategyManager:
|
|
|
957
958
|
job["deploy_retry_after"] = time.time() + min(10.0, 2.0 * count)
|
|
958
959
|
self._state(job, "error", str(error))
|
|
959
960
|
raise
|
|
961
|
+
if job.get("state") == "waiting_account":
|
|
962
|
+
# QMT creates its account authorization on first login.
|
|
963
|
+
# Do not fabricate a model key or make login depend on
|
|
964
|
+
# the model that can only be prepared after that login.
|
|
965
|
+
waiting_account.append(job["account_id"])
|
|
966
|
+
continue
|
|
960
967
|
if (job.get("state") not in ("configured", "waiting_import", "disabled")
|
|
961
968
|
or (job.get("enabled") and any(
|
|
962
969
|
not role.get("model_prepared") or role.get("reimport_required")
|
|
@@ -965,6 +972,7 @@ class QmtStrategyManager:
|
|
|
965
972
|
(job.get("message") or job.get("state", "")))
|
|
966
973
|
finally:
|
|
967
974
|
self._save()
|
|
975
|
+
return {"waiting_account": waiting_account}
|
|
968
976
|
|
|
969
977
|
def _advance(self, job):
|
|
970
978
|
root = qmt_root(job["root"])
|
|
@@ -81,6 +81,15 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
|
|
|
81
81
|
expect(button).to_be_disabled()
|
|
82
82
|
assert not page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.classList.contains('is-done')")
|
|
83
83
|
expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("等待切换")
|
|
84
|
+
for phase in ("extract", "activating", "restarting"):
|
|
85
|
+
page.evaluate("""phase => bindingPythonRuntimeProgressEvent({
|
|
86
|
+
meta: {job_id: state.bindingPythonRuntimeJobId, stage: phase},
|
|
87
|
+
data: {phase, percent: 0, eta: null}
|
|
88
|
+
})""", phase)
|
|
89
|
+
expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("0%")
|
|
90
|
+
expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("剩余约 0")
|
|
91
|
+
assert page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.getBoundingClientRect().width > 0")
|
|
92
|
+
assert page.evaluate("!!state.bindingPythonRuntimeJobId")
|
|
84
93
|
status["binding"].update(selected_runtime="py312", configured_runtime="py312", state="ready")
|
|
85
94
|
status["binding"]["runtimes"]["py312"]["installed"] = True
|
|
86
95
|
page.evaluate("""bindingPythonRuntimeProgressEvent({
|
|
@@ -95,6 +104,45 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
|
|
|
95
104
|
assert not errors
|
|
96
105
|
|
|
97
106
|
|
|
107
|
+
def test_lost_job_record_releases_busy_state_without_claiming_success(page, frontend_url):
|
|
108
|
+
errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
|
|
109
|
+
page.route("**/api/qmt/python-runtime/download", lambda route: route.fulfill(json={"ok": True, "data": {"state": "queued"}}))
|
|
110
|
+
page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(status=404, json={"ok": False, "error": "job not found"}))
|
|
111
|
+
page.locator("#bindingPythonRuntimeDownloadBtn").click()
|
|
112
|
+
page.wait_for_function("!state.bindingPythonRuntimeJobId && state.bindingPythonRuntimeProgress?.meta?.stage === 'error'")
|
|
113
|
+
expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("记录已失效")
|
|
114
|
+
expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_enabled()
|
|
115
|
+
assert not errors
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_save_rechecks_completed_runtime_job_even_without_progress_notification(page, frontend_url):
|
|
119
|
+
errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
|
|
120
|
+
page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
|
|
121
|
+
"ok": True, "data": {"state": "done", "result": {"account_config_required": True}}}))
|
|
122
|
+
saved = []
|
|
123
|
+
def save(route):
|
|
124
|
+
body = route.request.post_data_json
|
|
125
|
+
saved.append(body)
|
|
126
|
+
row = dict(body, account_key="TEST_ONLY|STOCK|test", bridge_id="test")
|
|
127
|
+
route.fulfill(json={"ok": True, "data": {
|
|
128
|
+
"account": row, "account_configs": {row["account_key"]: row}, "account_pairs": {},
|
|
129
|
+
"bridges": {"test": {"name": "Test"}}, "qmt_strategy_deploy": {"enabled": False},
|
|
130
|
+
"qmt_auto_login": {"enabled": False}}})
|
|
131
|
+
page.route("**/api/account-config", save)
|
|
132
|
+
page.locator("#bindingQmtAutoLogin").uncheck()
|
|
133
|
+
page.evaluate("""() => {
|
|
134
|
+
fillQmtStrategySettings('binding', {enabled: false});
|
|
135
|
+
state.bindingPythonRuntimeJobId = 'missed-done';
|
|
136
|
+
state.bindingPythonRuntimeJobContext = bindingPythonRuntimeContextKey();
|
|
137
|
+
state.bindingPythonRuntimeJobRuntime = 'py36';
|
|
138
|
+
}""")
|
|
139
|
+
page.locator('#bindingForm [type="submit"]').click()
|
|
140
|
+
expect(page.locator("#bindingDialogOverlay")).not_to_be_visible()
|
|
141
|
+
assert len(saved) == 1
|
|
142
|
+
assert not page.evaluate("state.bindingPythonRuntimeJobId")
|
|
143
|
+
assert not errors
|
|
144
|
+
|
|
145
|
+
|
|
98
146
|
def test_directory_change_invalidates_options_and_discards_late_response(page, frontend_url):
|
|
99
147
|
errors = open_runtime(page, frontend_url, runtime_status())
|
|
100
148
|
pending = []
|
|
@@ -129,3 +177,28 @@ def test_missing_account_config_shows_capabilities_but_requires_login(page, fron
|
|
|
129
177
|
page.wait_for_function("state.bindingPythonRuntimeJobId")
|
|
130
178
|
assert submitted[0]["allow_missing_account_config"] is True
|
|
131
179
|
assert not errors
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.parametrize("socket_throws", [False, True], ids=["socket-disconnected", "socket-unavailable"])
|
|
183
|
+
def test_poll_completion_before_first_login_unlocks_binding(page, frontend_url, socket_throws):
|
|
184
|
+
status = runtime_status(config_exists=False)
|
|
185
|
+
errors = open_runtime(page, frontend_url, status)
|
|
186
|
+
submitted = []
|
|
187
|
+
page.route("**/api/qmt/python-runtime/download", lambda route: (
|
|
188
|
+
submitted.append(route.request.post_data_json),
|
|
189
|
+
route.fulfill(json={"ok": True, "data": {"state": "queued"}})))
|
|
190
|
+
# The last progress event is not a terminal event. HTTP state/result must
|
|
191
|
+
# recover completion even if the WebSocket notification never arrived.
|
|
192
|
+
page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
|
|
193
|
+
"ok": True, "data": {"state": "done", "progress": {"phase": "installed", "percent": 100},
|
|
194
|
+
"result": {"account_config_required": True, "restarted": None}}}))
|
|
195
|
+
if socket_throws:
|
|
196
|
+
page.evaluate("() => { window.WebSocket = function() { throw new Error('WebSocket unavailable'); }; }")
|
|
197
|
+
page.locator("#bindingPythonRuntimeDownloadBtn").click()
|
|
198
|
+
page.wait_for_function("state.bindingPythonRuntimeJobId === '' && state.bindingPythonRuntimeProgress?.meta?.stage === 'done'")
|
|
199
|
+
assert len(submitted) == 1
|
|
200
|
+
assert page.evaluate("state.bindingPythonRuntimeDeferred")
|
|
201
|
+
assert not page.evaluate("state.bindingPythonRuntimeChoiceDirty")
|
|
202
|
+
expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("100%")
|
|
203
|
+
expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("登录")
|
|
204
|
+
assert not errors
|
|
@@ -8,6 +8,7 @@ from types import SimpleNamespace
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
10
|
from cfquant.tests.test_qmt_strategy_web import web_config, login_credentials
|
|
11
|
+
from cfquant.tests.test_qmt_strategy_deploy import deployment
|
|
11
12
|
from cfquant.tests.test_management import service
|
|
12
13
|
from cfquant.qmt_credentials import QmtCredentialStore
|
|
13
14
|
from cfquant.trading_calendar import is_trading_day
|
|
@@ -286,6 +287,37 @@ def test_failed_offline_deployment_blocks_launch_and_login(web_config, monkeypat
|
|
|
286
287
|
assert not result["started"] and not result["configured"]
|
|
287
288
|
|
|
288
289
|
|
|
290
|
+
@pytest.mark.parametrize("auto_fill", [True, False])
|
|
291
|
+
@pytest.mark.parametrize("restart", [True, False])
|
|
292
|
+
def test_first_login_without_model_key_launches_and_reports_pending_deployment(
|
|
293
|
+
web_config, deployment, monkeypatch, tmp_path, auto_fill, restart):
|
|
294
|
+
web, _ = web_config
|
|
295
|
+
manager, row, infos, running, root = deployment
|
|
296
|
+
(root / "config" / "indexUserConfig.xml").write_text(
|
|
297
|
+
'<ICUserConfigFile><FormulaCatalog/><strategyTrade/></ICUserConfigFile>', encoding="utf-8")
|
|
298
|
+
manager.configure(row, infos)
|
|
299
|
+
row["qmt_auto_login"] = {"enabled": True, "auto_fill_credentials": auto_fill, "auto_update": True}
|
|
300
|
+
monkeypatch.setenv("CFQUANT_QMT_AUTO_START", "saved")
|
|
301
|
+
monkeypatch.setattr(web, "QMT_AUTO_LOGIN_STATE_FILE", str(tmp_path / "sessions.json"))
|
|
302
|
+
monkeypatch.setattr(web, "QMT_COMPONENT_JOBS", {})
|
|
303
|
+
monkeypatch.setattr(web, "QMT_STRATEGIES", manager)
|
|
304
|
+
paths = {"bin_dir": str(root / "bin.x64"), "exe_path": "fixture.exe"}
|
|
305
|
+
monkeypatch.setattr(web, "_qmt_auto_login_paths", lambda _: paths)
|
|
306
|
+
monkeypatch.setattr(web, "_qmt_component_login_preflight", lambda *a: None)
|
|
307
|
+
monkeypatch.setattr(web, "_qmt_auto_login_processes", lambda *a, **k: [])
|
|
308
|
+
monkeypatch.setattr(web, "_qmt_auto_login_stop_xtitclient", lambda *a, **k: {})
|
|
309
|
+
calls = []
|
|
310
|
+
monkeypatch.setattr(web, "_qmt_auto_login_start_xtitclient", lambda *a, **k: calls.append("start") or {"pid": 456})
|
|
311
|
+
monkeypatch.setattr(web, "_qmt_queue_component_login", lambda *a: calls.append("login"))
|
|
312
|
+
result = web.qmt_auto_login_apply_for_account(row, restart=restart)
|
|
313
|
+
assert result["configured"] and not result.get("error")
|
|
314
|
+
assert result["strategy_deploy_deferred"]
|
|
315
|
+
assert calls == ["start", "login"]
|
|
316
|
+
session = web._qmt_auto_login_saved_session(row)
|
|
317
|
+
assert session["strategy_deploy_deferred"] and not session["error"]
|
|
318
|
+
assert "首次登录" in session["message"]
|
|
319
|
+
|
|
320
|
+
|
|
289
321
|
def test_worker_records_success_and_supplies_saved_password_only_on_request(web_config, monkeypatch, tmp_path):
|
|
290
322
|
import cfquant.qmt_component_login as component
|
|
291
323
|
web, config = web_config
|