Skip to content

Commit

Permalink
Merge pull request #7 from flisky/master
Browse files Browse the repository at this point in the history
support CN endpoints
  • Loading branch information
sunli829 authored Nov 30, 2023
2 parents 42ba53f + 3d8886c commit 51eb304
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 41 deletions.
8 changes: 4 additions & 4 deletions python/pysrc/longport/openapi.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 22 additions & 15 deletions python/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
quote_ws_url: Option<String>,
trade_ws_url: Option<String>,
language: Option<Language>,
) -> 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]
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/httpclient/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
6 changes: 4 additions & 2 deletions rust/crates/httpclient/src/config.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -22,8 +23,9 @@ impl HttpClientConfig {
app_secret: impl Into<String>,
access_token: impl Into<String>,
) -> 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(),
Expand Down
80 changes: 80 additions & 0 deletions rust/crates/httpclient/src/geo.rs
Original file line number Diff line number Diff line change
@@ -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<Option<Instant>> = Cell::new(None);
static LAST_PING_REGION: RefCell<String> = RefCell::new(String::new());
}

fn region() -> Option<String> {
// 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));
}
}
2 changes: 2 additions & 0 deletions rust/crates/httpclient/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
mod client;
mod config;
mod error;
mod geo;
mod qs;
mod request;
mod signature;
Expand All @@ -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;
47 changes: 28 additions & 19 deletions rust/src/config.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 51eb304

Please sign in to comment.