cfquant 0.2.70__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.70 → cfquant-0.2.71}/PKG-INFO +1 -1
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_python_runtime.py +175 -53
- cfquant-0.2.71/cfquant/tests/test_qmt_python_mirror.py +181 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/version.py +2 -2
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/SOURCES.txt +1 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/pyproject.toml +1 -1
- {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT_LITE.py +1 -1
- {cfquant-0.2.70 → 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.70 → 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.70 → cfquant-0.2.71}/web_dashboard/app.js +1 -1
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/index.html +2 -2
- {cfquant-0.2.70 → cfquant-0.2.71}/LICENSE +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/__init__.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/MANIFEST.in +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/README.md +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/__init__.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/algo.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/build_info.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/channels.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/cli.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/client.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/conditional_orders.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/config.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/diagnostic_context.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/hub.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/level2.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/log_management.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/management.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/monitor_notifications.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/monitor_performance.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/notify.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/planning_broker.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/position_sizing.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/protocol.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_component_login.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_inner.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_inner_catalog.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_inner_runtime.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_mac_agent.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_native_files.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_native_worker.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_agent.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_agent.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_discovery.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_runtime.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_settings.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/qmt_vm_worker.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/smart_orders.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_bridge.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_client.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_manager.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/subscription_store.py +0 -0
- {cfquant-0.2.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → cfquant-0.2.71}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_activity_log_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_auto_login_app.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_python_runtime_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_vm_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_binding_webhook_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_diagnostic_context.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_history_download_memory.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_monitor_notifications.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_monitor_performance.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_navigation_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_notify.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_auto_login_integration.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_reference.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_inner_web.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_files.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_reuse.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_native_worker.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_package_import.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_python_runtime.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_native.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_qmt_vm_settings.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_smart_orders.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_api.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_smart_orders_docs.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_split_order_identity.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_strategy_recovery_api.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_subscription_persistence.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_trading_monitor_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_version_check.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tests/test_version_check_browser.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/THIRD_PARTY.md +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/COPYING +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/LICENSE +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/NEWS +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/README +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/THANKS.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.html +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx-doc.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx.1 +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tools/upx/upx.exe +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/trading_monitor.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tx.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/tx_trade_bridge.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/webhook.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant/xttype.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/cfquant_web_server.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.70 → 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.70 → 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.70 → 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.70 → 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.70 → 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.70 → cfquant-0.2.71}/requirements.txt +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/setup.cfg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/activity-log.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/cfquant-architecture.html +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/navigation.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/notify-api-data.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/qmt-inner-api-data.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/smart-order-api-data.js +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.70 → cfquant-0.2.71}/web_dashboard/trading-monitor.js +0 -0
- {cfquant-0.2.70 → 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)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Source routing, byte integrity and cancellation without contacting live QMT."""
|
|
2
|
+
from dataclasses import replace
|
|
3
|
+
import hashlib
|
|
4
|
+
import http.client
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import urllib.error
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from cfquant import qmt_python_runtime as runtime
|
|
13
|
+
|
|
14
|
+
DATA = b'QMT test runtime archive' * 100
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Response(io.BytesIO):
|
|
18
|
+
def __init__(self, data=DATA, status=200, headers=None):
|
|
19
|
+
super().__init__(data)
|
|
20
|
+
self.status = status
|
|
21
|
+
self.headers = headers or {'Content-Length': str(len(data))}
|
|
22
|
+
|
|
23
|
+
def getcode(self):
|
|
24
|
+
return self.status
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def setup(tmp_path, monkeypatch):
|
|
29
|
+
manager = runtime.QmtPythonRuntime(tmp_path)
|
|
30
|
+
package = runtime.QmtPythonPackage('py312', '2.0.3.12', 'http://xuntou.test/runtime.zip',
|
|
31
|
+
'runtime.zip', hashlib.md5(DATA).hexdigest(), True,
|
|
32
|
+
hashlib.sha256(DATA).hexdigest(), len(DATA))
|
|
33
|
+
snapshot = dict(ok=True, schema=1, busy=False, packages={'py312': package.as_dict()})
|
|
34
|
+
monkeypatch.setattr(manager, '_mirror_status', lambda: snapshot)
|
|
35
|
+
calls = []
|
|
36
|
+
def request(url, **kwargs):
|
|
37
|
+
calls.append((url, kwargs.get('headers', {})))
|
|
38
|
+
return Response()
|
|
39
|
+
monkeypatch.setattr(manager, '_request', request)
|
|
40
|
+
return manager, package, snapshot, calls
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_mirror_first_and_verified_cache_needs_no_network(setup, monkeypatch):
|
|
44
|
+
manager, package, snapshot, calls = setup
|
|
45
|
+
assert manager.fetch_package() == package
|
|
46
|
+
assert manager.download(package=package).read_bytes() == DATA
|
|
47
|
+
assert calls[0][0].startswith(runtime._MIRROR_BASE)
|
|
48
|
+
assert calls[0][1][runtime._MIRROR_KEY_HEADER] == runtime._MIRROR_KEY
|
|
49
|
+
monkeypatch.setattr(manager, '_mirror_status', lambda: pytest.fail('cached file should work offline'))
|
|
50
|
+
manager.download(package=package)
|
|
51
|
+
assert len(calls) == 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_busy_uses_upstream_without_mirror_auth_header(setup):
|
|
55
|
+
manager, package, snapshot, calls = setup
|
|
56
|
+
snapshot['busy'] = True
|
|
57
|
+
manager.download(package=package)
|
|
58
|
+
assert calls == [(package.url, {'Accept-Encoding': 'identity'})]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize('busy', [True, False])
|
|
62
|
+
def test_upstream_failure_forces_website_even_when_busy(setup, monkeypatch, busy):
|
|
63
|
+
manager, package, snapshot, calls = setup
|
|
64
|
+
snapshot['busy'] = busy
|
|
65
|
+
def request(url, **kwargs):
|
|
66
|
+
calls.append(url)
|
|
67
|
+
if 'force=1' not in url:
|
|
68
|
+
raise urllib.error.URLError('offline')
|
|
69
|
+
return Response()
|
|
70
|
+
monkeypatch.setattr(manager, '_request', request)
|
|
71
|
+
assert manager.download(package=package).read_bytes() == DATA
|
|
72
|
+
assert calls[-1].endswith('&force=1')
|
|
73
|
+
assert calls[-2] == package.url
|
|
74
|
+
assert len(calls) == (2 if busy else 3)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_unavailable_status_still_attempts_direct_forced_download(setup, monkeypatch):
|
|
78
|
+
manager, package, snapshot, calls = setup
|
|
79
|
+
monkeypatch.setattr(manager, '_mirror_status', lambda: None)
|
|
80
|
+
def request(url, **kwargs):
|
|
81
|
+
calls.append(url)
|
|
82
|
+
if url == package.url:
|
|
83
|
+
raise TimeoutError('timeout')
|
|
84
|
+
return Response()
|
|
85
|
+
monkeypatch.setattr(manager, '_request', request)
|
|
86
|
+
manager.download(package=package)
|
|
87
|
+
assert len(calls) == 2 and calls[-1].endswith('&force=1')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_corrupt_download_is_discarded_before_fallback(setup, monkeypatch):
|
|
91
|
+
manager, package, snapshot, calls = setup
|
|
92
|
+
def request(url, **kwargs):
|
|
93
|
+
calls.append((url, kwargs['headers']))
|
|
94
|
+
return Response(b'x' * len(DATA)) if len(calls) == 1 else Response()
|
|
95
|
+
monkeypatch.setattr(manager, '_request', request)
|
|
96
|
+
assert manager.download(package=package).read_bytes() == DATA
|
|
97
|
+
assert calls[-1][0] == package.url and 'Range' not in calls[-1][1]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_truncated_response_resumes_at_other_source(setup, monkeypatch):
|
|
101
|
+
manager, package, snapshot, calls = setup
|
|
102
|
+
def request(url, **kwargs):
|
|
103
|
+
calls.append((url, kwargs['headers']))
|
|
104
|
+
if len(calls) == 1:
|
|
105
|
+
return Response(DATA[:100], headers={'Content-Length': str(len(DATA))})
|
|
106
|
+
assert kwargs['headers']['Range'] == 'bytes=100-'
|
|
107
|
+
return Response(DATA[100:], status=206,
|
|
108
|
+
headers={'Content-Range': 'bytes 100-%d/%d' % (len(DATA)-1, len(DATA))})
|
|
109
|
+
monkeypatch.setattr(manager, '_request', request)
|
|
110
|
+
assert manager.download(package=package).read_bytes() == DATA
|
|
111
|
+
assert len(calls) == 2
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_incorrect_range_cannot_be_appended(setup, monkeypatch):
|
|
115
|
+
manager, package, snapshot, calls = setup
|
|
116
|
+
def request(url, **kwargs):
|
|
117
|
+
calls.append(url)
|
|
118
|
+
if len(calls) == 1:
|
|
119
|
+
return Response(DATA[:100], headers={'Content-Length': str(len(DATA))})
|
|
120
|
+
if len(calls) == 2:
|
|
121
|
+
return Response(DATA, status=206, headers={'Content-Range': 'bytes 0-%d/%d' % (len(DATA)-1, len(DATA))})
|
|
122
|
+
return Response() # A server may ignore Range and send a complete 200.
|
|
123
|
+
monkeypatch.setattr(manager, '_request', request)
|
|
124
|
+
assert manager.download(package=package).read_bytes() == DATA
|
|
125
|
+
assert len(calls) == 3
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_sha256_failure_cannot_be_installed(setup, monkeypatch):
|
|
129
|
+
manager, package, snapshot, calls = setup
|
|
130
|
+
package = replace(package, sha256='0' * 64)
|
|
131
|
+
with pytest.raises(runtime.QmtPythonRuntimeError, match='SHA-256 mismatch'):
|
|
132
|
+
manager.download(package=package)
|
|
133
|
+
assert len(calls) == 3
|
|
134
|
+
assert not (manager.download_dir / package.filename).exists()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_cancel_does_not_fall_back(setup):
|
|
138
|
+
manager, package, snapshot, calls = setup
|
|
139
|
+
cancel = threading.Event()
|
|
140
|
+
def progress(event):
|
|
141
|
+
if event['bytes_done']:
|
|
142
|
+
cancel.set()
|
|
143
|
+
with pytest.raises(runtime.QmtPythonRuntimeError, match='cancelled'):
|
|
144
|
+
manager.download(package=package, cancel_event=cancel, progress_callback=progress)
|
|
145
|
+
assert len(calls) == 1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_local_write_failure_does_not_fall_back(setup, monkeypatch):
|
|
149
|
+
manager, package, snapshot, calls = setup
|
|
150
|
+
monkeypatch.setattr(runtime.os, 'fsync', lambda fd: (_ for _ in ()).throw(OSError('disk full')))
|
|
151
|
+
with pytest.raises(OSError, match='disk full'):
|
|
152
|
+
manager.download(package=package)
|
|
153
|
+
assert len(calls) == 1
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_config_fallback_and_status_timeout_cache(tmp_path, monkeypatch):
|
|
157
|
+
manager = runtime.QmtPythonRuntime(tmp_path)
|
|
158
|
+
calls = []
|
|
159
|
+
def request(url, **kwargs):
|
|
160
|
+
calls.append((url, kwargs))
|
|
161
|
+
if url.startswith(runtime._MIRROR_BASE):
|
|
162
|
+
raise TimeoutError('offline')
|
|
163
|
+
return Response(b'[python]\nversion=2.0.3.12\nurl=http://xuntou.test/runtime.zip\nfilename=runtime.zip\nmd5=' + b'1'*32)
|
|
164
|
+
monkeypatch.setattr(manager, '_request', request)
|
|
165
|
+
assert manager.fetch_package().md5 == '1'*32
|
|
166
|
+
assert manager.fetch_package().md5 == '1'*32
|
|
167
|
+
assert len(calls) == 3
|
|
168
|
+
assert calls[0][1]['timeout'] == 5
|
|
169
|
+
assert 'headers' not in calls[-1][1]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_manifest_version_mismatch_never_uses_normal_mirror(setup):
|
|
173
|
+
manager, package, snapshot, calls = setup
|
|
174
|
+
snapshot['packages']['py312']['md5'] = '0'*32
|
|
175
|
+
manager.download(package=package)
|
|
176
|
+
assert calls[0][0] == package.url
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_key_is_not_forwarded_on_redirect():
|
|
180
|
+
handler = runtime._NoMirrorRedirect()
|
|
181
|
+
assert handler.redirect_request(None, None, 302, '', {}, 'https://third-party.test') is None
|
|
@@ -198,6 +198,7 @@ cfquant/tests/test_qmt_native_files.py
|
|
|
198
198
|
cfquant/tests/test_qmt_native_reuse.py
|
|
199
199
|
cfquant/tests/test_qmt_native_worker.py
|
|
200
200
|
cfquant/tests/test_qmt_package_import.py
|
|
201
|
+
cfquant/tests/test_qmt_python_mirror.py
|
|
201
202
|
cfquant/tests/test_qmt_python_runtime.py
|
|
202
203
|
cfquant/tests/test_qmt_strategy_browser.py
|
|
203
204
|
cfquant/tests/test_qmt_strategy_capabilities.py
|
|
@@ -753,7 +753,7 @@ def _resolve_batch_order_ids(bridge, account, pending, before_ids):
|
|
|
753
753
|
time.sleep(min(0.05, remaining))
|
|
754
754
|
# END GENERATED CFTRADER BATCH
|
|
755
755
|
|
|
756
|
-
CORE_VERSION = "0.2.
|
|
756
|
+
CORE_VERSION = "0.2.71"
|
|
757
757
|
|
|
758
758
|
# BEGIN GENERATED DIAGNOSTIC CONTEXT
|
|
759
759
|
"""Version context for cfquant diagnostics without changing application logging."""
|
|
@@ -767,7 +767,7 @@ def _resolve_batch_order_ids(bridge, account, pending, before_ids):
|
|
|
767
767
|
time.sleep(min(0.05, remaining))
|
|
768
768
|
# END GENERATED CFTRADER BATCH
|
|
769
769
|
|
|
770
|
-
CORE_VERSION = "0.2.
|
|
770
|
+
CORE_VERSION = "0.2.71"
|
|
771
771
|
|
|
772
772
|
# BEGIN GENERATED DIAGNOSTIC CONTEXT
|
|
773
773
|
"""Version context for cfquant diagnostics without changing application logging."""
|
|
@@ -767,7 +767,7 @@ def _resolve_batch_order_ids(bridge, account, pending, before_ids):
|
|
|
767
767
|
time.sleep(min(0.05, remaining))
|
|
768
768
|
# END GENERATED CFTRADER BATCH
|
|
769
769
|
|
|
770
|
-
CORE_VERSION = "0.2.
|
|
770
|
+
CORE_VERSION = "0.2.71"
|
|
771
771
|
|
|
772
772
|
# BEGIN GENERATED DIAGNOSTIC CONTEXT
|
|
773
773
|
"""Version context for cfquant diagnostics without changing application logging."""
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>cfquant 控制台</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
8
|
+
<link rel="stylesheet" href="/styles.css?v=web_20261112_02">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="webAuthOverlay" class="auth-overlay hidden" aria-live="polite">
|
|
@@ -3585,7 +3585,7 @@ else:
|
|
|
3585
3585
|
<script src="/trading-monitor.js?v=20261111_runtime_window_01"></script>
|
|
3586
3586
|
<script src="/navigation.js?v=web_20261108_nav_03"></script>
|
|
3587
3587
|
<script src="/activity-log.js?v=web_20261108_logs_01"></script>
|
|
3588
|
-
<script src="/app.js?v=
|
|
3588
|
+
<script src="/app.js?v=web_20261112_02"></script>
|
|
3589
3589
|
<script src="/remote-management.js?v=20261103"></script>
|
|
3590
3590
|
</body>
|
|
3591
3591
|
</html>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|