From 1a969fcf13b16f333853c3bfa720025c98236200 Mon Sep 17 00:00:00 2001 From: Yin Jifeng Date: Wed, 29 Nov 2023 21:16:20 +0800 Subject: [PATCH 1/3] feat: support `is_cn` with env var & network cache --- rust/crates/httpclient/Cargo.toml | 2 +- rust/crates/httpclient/src/geo.rs | 80 +++++++++++++++++++++++++++++++ rust/crates/httpclient/src/lib.rs | 2 + 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 rust/crates/httpclient/src/geo.rs diff --git a/rust/crates/httpclient/Cargo.toml b/rust/crates/httpclient/Cargo.toml index 17380127e..72b190a4e 100644 --- a/rust/crates/httpclient/Cargo.toml +++ b/rust/crates/httpclient/Cargo.toml @@ -19,7 +19,7 @@ sha1 = "0.10.1" sha2 = "0.10.2" thiserror = "1.0.31" tracing = { version = "0.1.34", features = ["attributes"] } -tokio = { version = "1.18.2", features = ["time"] } +tokio = { version = "1.18.2", features = ["rt", "time"] } percent-encoding = "2.1.0" dotenv = "0.15.0" diff --git a/rust/crates/httpclient/src/geo.rs b/rust/crates/httpclient/src/geo.rs new file mode 100644 index 000000000..1a47cc300 --- /dev/null +++ b/rust/crates/httpclient/src/geo.rs @@ -0,0 +1,80 @@ +use std::{ + cell::{Cell, RefCell}, + time::{Duration, Instant}, +}; + +// because we may call `is_cn` multi times in a short time, we cache the result +thread_local! { + static LAST_PING: Cell> = Cell::new(None); + static LAST_PING_REGION: RefCell = RefCell::new(String::new()); +} + +fn region() -> Option { + // check user defined REGION + if let Ok(region) = std::env::var("LONGPORT_REGION") { + return Some(region); + } + + // check network connectivity + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async { + if let Some(last_ping) = LAST_PING.get() { + if last_ping.elapsed() < Duration::from_secs(60) { + return Some(LAST_PING_REGION.with_borrow(Clone::clone)); + } + } + let Ok(resp) = reqwest::Client::new() + .get("https://api.lbkrs.com/_ping") + .timeout(Duration::from_secs(1)) + .send() + .await + else { + return None; + }; + let Some(region) = resp + .headers() + .get("X-Ip-Region") + .and_then(|v| v.to_str().ok()) + else { + return None; + }; + LAST_PING.set(Some(Instant::now())); + LAST_PING_REGION.replace(region.to_string()); + Some(region.to_string()) + }) +} + +/// do the best to guess whether the access point is in China Mainland or not +pub fn is_cn() -> bool { + region().map_or(false, |region| region.eq_ignore_ascii_case("CN")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_var() { + std::env::set_var("LONGPORT_REGION", "CN"); + assert!(is_cn()); + + std::env::set_var("LONGPORT_REGION", "SG"); + assert!(!is_cn()); + } + + #[test] + fn test_network() { + std::env::remove_var("LONGPORT_REGION"); + // should be a refresh executed + let result = is_cn(); + + // should shot the cache + let start = Instant::now(); + assert_eq!(result, is_cn()); + // 500us should be less than a http request, and greater than local calc + assert!(start.elapsed() < Duration::from_micros(500)); + } +} diff --git a/rust/crates/httpclient/src/lib.rs b/rust/crates/httpclient/src/lib.rs index 15ca94752..59af8200a 100644 --- a/rust/crates/httpclient/src/lib.rs +++ b/rust/crates/httpclient/src/lib.rs @@ -8,6 +8,7 @@ mod client; mod config; mod error; +mod geo; mod qs; mod request; mod signature; @@ -16,6 +17,7 @@ mod timestamp; pub use client::HttpClient; pub use config::HttpClientConfig; pub use error::{HttpClientError, HttpClientResult}; +pub use geo::is_cn; pub use qs::QsError; pub use request::{FromPayload, Json, RequestBuilder, ToPayload}; pub use reqwest::Method; From 78c297b6a7fdea657331437593a878c9cf2a4a1a Mon Sep 17 00:00:00 2001 From: Yin Jifeng Date: Wed, 29 Nov 2023 21:36:19 +0800 Subject: [PATCH 2/3] chore: support CN endpoints --- rust/crates/httpclient/src/config.rs | 6 ++-- rust/src/config.rs | 47 +++++++++++++++++----------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/rust/crates/httpclient/src/config.rs b/rust/crates/httpclient/src/config.rs index 9628f731d..28fee6594 100644 --- a/rust/crates/httpclient/src/config.rs +++ b/rust/crates/httpclient/src/config.rs @@ -1,6 +1,7 @@ -use crate::HttpClientError; +use crate::{is_cn, HttpClientError}; const HTTP_URL: &str = "https://openapi.longportapp.com"; +const CN_HTTP_URL: &str = "https://openapi.longportapp.cn"; /// Configuration options for Http client #[derive(Debug, Clone)] @@ -22,8 +23,9 @@ impl HttpClientConfig { app_secret: impl Into, access_token: impl Into, ) -> Self { + let http_url = if is_cn() { CN_HTTP_URL } else { HTTP_URL }; Self { - http_url: HTTP_URL.to_string(), + http_url: http_url.to_string(), app_key: app_key.into(), app_secret: app_secret.into(), access_token: access_token.into(), diff --git a/rust/src/config.rs b/rust/src/config.rs index 6f2217902..bbfdf1148 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -1,6 +1,6 @@ use http::Method; pub(crate) use http::{header, HeaderValue, Request}; -use longport_httpcli::{HttpClient, HttpClientConfig, Json}; +use longport_httpcli::{is_cn, HttpClient, HttpClientConfig, Json}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use tokio_tungstenite::tungstenite::client::IntoClientRequest; @@ -9,6 +9,8 @@ use crate::error::Result; const QUOTE_WS_URL: &str = "wss://openapi-quote.longportapp.com/v2"; const TRADE_WS_URL: &str = "wss://openapi-trade.longportapp.com/v2"; +const CN_QUOTE_WS_URL: &str = "wss://openapi-quote.longportapp.cn/v2"; +const CN_TRADE_WS_URL: &str = "wss://openapi-trade.longportapp.cn/v2"; /// Language identifier #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -75,26 +77,33 @@ impl Config { let _ = dotenv::dotenv(); let http_cli_config = HttpClientConfig::from_env()?; - let mut config = Config { - http_cli_config, - quote_ws_url: QUOTE_WS_URL.to_string(), - trade_ws_url: TRADE_WS_URL.to_string(), - language: Language::EN, - }; - - if let Ok(quote_ws_url) = std::env::var("LONGBRIDGE_QUOTE_WS_URL") + let quote_ws_url = std::env::var("LONGBRIDGE_QUOTE_WS_URL") .or_else(|_| std::env::var("LONGPORT_QUOTE_WS_URL")) - { - config.quote_ws_url = quote_ws_url; - } - - if let Ok(trade_ws_url) = std::env::var("LONGBRIDGE_TRADE_WS_URL") + .unwrap_or_else(|_| { + if is_cn() { + CN_QUOTE_WS_URL + } else { + QUOTE_WS_URL + } + .to_string() + }); + let trade_ws_url = std::env::var("LONGBRIDGE_TRADE_WS_URL") .or_else(|_| std::env::var("LONGPORT_TRADE_WS_URL")) - { - config.trade_ws_url = trade_ws_url; - } - - Ok(config) + .unwrap_or_else(|_| { + if is_cn() { + CN_TRADE_WS_URL + } else { + TRADE_WS_URL + } + .to_string() + }); + + Ok(Config { + http_cli_config, + quote_ws_url, + trade_ws_url, + language: Language::EN, + }) } /// Specifies the url of the OpenAPI server. From 3d8886cec994e2f211f149bb7f57517ce5a5f85e Mon Sep 17 00:00:00 2001 From: Yin Jifeng Date: Wed, 29 Nov 2023 21:41:49 +0800 Subject: [PATCH 3/3] chore: unset python's default config --- python/pysrc/longport/openapi.pyi | 8 +++---- python/src/config.rs | 37 ++++++++++++++++++------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/python/pysrc/longport/openapi.pyi b/python/pysrc/longport/openapi.pyi index 9ec276f28..d9ffb1a91 100644 --- a/python/pysrc/longport/openapi.pyi +++ b/python/pysrc/longport/openapi.pyi @@ -92,10 +92,10 @@ class Config: app_key: str, app_secret: str, access_token: str, - http_url: str = "https://openapi.longportapp.com", - quote_ws_url: str = "wss://openapi-quote.longportapp.com/v2", - trade_ws_url: str = "wss://openapi-trade.longportapp.com/v2", - language: Type[Language] = Language.EN, + http_url: Optional[str] = None, + quote_ws_url: Optional[str] = None, + trade_ws_url: Optional[str] = None, + language: Optional[Type[Language]] = None, ) -> None: ... @classmethod diff --git a/python/src/config.rs b/python/src/config.rs index 868d69583..d683ac095 100644 --- a/python/src/config.rs +++ b/python/src/config.rs @@ -12,27 +12,34 @@ impl Config { app_key, app_secret, access_token, - http_url = "https://openapi.longportapp.com", - quote_ws_url = "wss://openapi-quote.longportapp.com/v2", - trade_ws_url = "wss://openapi-trade.longportapp.com/v2", - language = Language::EN, + http_url = None, + quote_ws_url = None, + trade_ws_url = None, + language = None, ))] fn py_new( app_key: String, app_secret: String, access_token: String, - http_url: &str, - quote_ws_url: &str, - trade_ws_url: &str, - language: Language, + http_url: Option, + quote_ws_url: Option, + trade_ws_url: Option, + language: Option, ) -> Self { - Self( - longport::Config::new(app_key, app_secret, access_token) - .http_url(http_url) - .quote_ws_url(quote_ws_url) - .trade_ws_url(trade_ws_url) - .language(language.into()), - ) + let mut config = longport::Config::new(app_key, app_secret, access_token); + if let Some(http_url) = http_url { + config = config.http_url(http_url); + } + if let Some(quote_ws_url) = quote_ws_url { + config = config.quote_ws_url(quote_ws_url); + } + if let Some(trade_ws_url) = trade_ws_url { + config = config.trade_ws_url(trade_ws_url); + } + if let Some(language) = language { + config = config.language(language.into()); + } + Self(config) } #[classmethod]