aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Edgecumbe <git@esotericnonsense.com>2019-10-27 03:13:09 +0100
committerDaniel Edgecumbe <git@esotericnonsense.com>2019-10-27 03:13:09 +0100
commite2f3538226095e2b01df7d2bf941530784d7b89b (patch)
treee687b99c111f81ac25c1564313f8227f5dfae57e
parent7f2d53e82035dc3c2106d1fc98c034aa0c4a7be7 (diff)
Split out client.rs
-rwxr-xr-xgenapi/main.py2
-rw-r--r--src/client.rs237
-rw-r--r--src/generated_methods.rs2
-rw-r--r--src/lib.rs228
4 files changed, 243 insertions, 226 deletions
diff --git a/genapi/main.py b/genapi/main.py
index b162fe1..8dad33d 100755
--- a/genapi/main.py
+++ b/genapi/main.py
@@ -716,7 +716,7 @@ def main() -> None:
]
for l in a:
f.write(l + "\n")
- f.write("impl crate::BFClient {\n")
+ f.write("impl crate::client::BFClient {\n")
for l in rust_operations.functions:
f.write(l + "\n")
f.write("}\n")
diff --git a/src/client.rs b/src/client.rs
new file mode 100644
index 0000000..faca372
--- /dev/null
+++ b/src/client.rs
@@ -0,0 +1,237 @@
+// SPDX-Copyright: Copyright (c) 2019 Daniel Edgecumbe (esotericnonsense)
+// SPDX-License-Identifier: AGPL-3.0-only
+//
+// This file is part of botfair. botfair is free software: you can
+// redistribute it and/or modify it under the terms of the GNU Affero General
+// Public License as published by the Free Software Foundation, either version
+// 3 of the License, or (at your option) any later version.
+//
+// botfair is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+// for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with botfair. If not, see <http://www.gnu.org/licenses/>.
+
+use crate::json_rpc::{RpcRequest, RpcResponse};
+use crate::result::{Error, Result};
+use reqwest::{Client, Identity};
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use std::sync::{Arc, RwLock};
+
+#[derive(Debug, Serialize)]
+struct LoginRequestForm {
+ username: String,
+ password: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[allow(non_snake_case)]
+struct LoginResponse {
+ sessionToken: Option<String>,
+ loginStatus: String, // TODO enum this
+}
+
+pub struct BFCredentials {
+ username: String,
+ password: String,
+ pfx: Vec<u8>,
+ app_key: String,
+}
+
+impl BFCredentials {
+ pub fn new(
+ username: String,
+ password: String,
+ pfx_path: String,
+ app_key: String,
+ ) -> Result<Self> {
+ let pfx = std::fs::read(pfx_path)?;
+ Ok(BFCredentials {
+ username,
+ password,
+ pfx,
+ app_key,
+ })
+ }
+
+ fn as_login_request_form(&self) -> LoginRequestForm {
+ LoginRequestForm {
+ username: self.username.clone(),
+ password: self.password.clone(),
+ }
+ }
+ fn pfx(&self) -> &Vec<u8> {
+ &self.pfx
+ }
+ fn app_key(&self) -> &String {
+ &self.app_key
+ }
+}
+
+pub struct BFClient {
+ client: reqwest::Client,
+ session_token: Arc<RwLock<Option<String>>>,
+ creds: BFCredentials,
+ proxy_uri: Option<String>,
+}
+
+impl BFClient {
+ pub fn new(
+ creds: BFCredentials,
+ proxy_uri: Option<String>,
+ ) -> Result<Self> {
+ let client: reqwest::Client = match &proxy_uri {
+ Some(uri) => {
+ let proxy = reqwest::Proxy::all(uri)?;
+ Client::builder().proxy(proxy).build()?
+ }
+ None => reqwest::Client::new(),
+ };
+ Ok(BFClient {
+ client,
+ session_token: Arc::new(RwLock::new(None)),
+ creds,
+ proxy_uri,
+ })
+ }
+
+ // TODO keepalive
+ // https://identitysso.betfair.com/api/keepAlive
+ // Accept (mandatory)
+ // Header that signals that the response should be returned as JSON application/json
+ // X-Authentication (mandatory)
+ // Header that represents the session token that needs to be keep alive Session Token
+ // X-Application (optional)
+ // Header the Application Key used by the customer to identify the product. App Key
+ // Response structure
+ //
+ //
+ // {
+ // "token":"<token_passed_as_header>",
+ // "product":"product_passed_as_header",
+ // "status":"<status>",
+ // "error":"<error>"
+ // }
+ // Status values
+ //
+ //
+ // SUCCESS
+ // FAIL
+ // Error values
+ //
+ //
+ // INPUT_VALIDATION_ERROR
+ // INTERNAL_ERROR
+ // NO_SESSION
+
+ // general notes
+ // We would therefore recommend that all Betfair API request are sent with the ‘Accept-Encoding: gzip, deflate’ request header.
+ // We recommend that Connection: keep-alive header is set for all requests to guarantee a persistent connection and therefore reducing latency. Please note: Idle keep-alive connection to the API endpoints are closed every 3 minutes.
+ // You should ensure that you handle the INVALID_SESSION_TOKEN error within your code by creating a new session token via the API login method.
+
+ fn req_internal<T1: Serialize, T2: DeserializeOwned>(
+ &self,
+ maybe_token: &Option<String>,
+ rpc_request: &RpcRequest<T1>,
+ ) -> Result<RpcResponse<T2>> {
+ match maybe_token {
+ None => Err(Error::General(
+ "req_internal: must login first".to_owned(),
+ )),
+ Some(token) => {
+ const JSONRPC_URI: &str =
+ "https://api.betfair.com/exchange/betting/json-rpc/v1";
+
+ Ok(self
+ .client
+ .post(JSONRPC_URI)
+ .header("X-Application", self.creds.app_key())
+ .header("X-Authentication", token)
+ .json(&rpc_request)
+ .send()?
+ .json()
+ .unwrap())
+ }
+ }
+ }
+
+ /// Perform a request, logging in if necessary, fail if login
+ pub fn req<T1: Serialize, T2: DeserializeOwned>(
+ &self,
+ req: RpcRequest<T1>,
+ ) -> Result<RpcResponse<T2>> {
+ // Initially acquire the token via a read lock
+
+ trace!("Taking token read lock");
+ let token_lock = self.session_token.read().unwrap();
+ let mut token = token_lock.clone();
+ drop(token_lock);
+ trace!("Dropped token read lock");
+
+ loop {
+ // TODO: exponential backoff
+
+ info!("Performing a request");
+ match self.req_internal(&token, &req) {
+ Ok(resp) => return Ok(resp),
+ Err(_) => {
+ info!("Not logged in");
+ // Assume the only error possible is an auth error
+
+ trace!("Taking token write lock");
+ let mut token_lock = self.session_token.write().unwrap();
+
+ if *token_lock == token {
+ *token_lock = Some(self.login()?);
+ }
+ token = token_lock.clone();
+
+ drop(token_lock); // drops at end of scope but we log
+ trace!("Dropped token read lock");
+ }
+ }
+ }
+ }
+
+ fn login(&self) -> Result<String> {
+ const CERTLOGIN_URI: &str =
+ "https://identitysso-cert.betfair.com/api/certlogin";
+
+ let ident =
+ Identity::from_pkcs12_der(self.creds.pfx().as_slice(), "")?;
+
+ let client: reqwest::Client = match &(self.proxy_uri) {
+ Some(uri) => {
+ let proxy = reqwest::Proxy::all(uri)?;
+ Client::builder().identity(ident).proxy(proxy).build()?
+ }
+ None => Client::builder().identity(ident).build()?,
+ };
+
+ let login_request_form = self.creds.as_login_request_form();
+
+ info!("LoginRequest ...");
+ let login_response: LoginResponse = client
+ .post(CERTLOGIN_URI)
+ .header(
+ "X-Application",
+ format!("schroedinger_{}", rand::random::<u128>()),
+ )
+ .form(&login_request_form)
+ .send()?
+ .json()?;
+
+ info!("LoginResponse: {:?}", login_response.loginStatus);
+
+ match login_response.sessionToken {
+ Some(token) => Ok(token),
+ None => Err(Error::BFLoginFailure(format!(
+ "loginStatus: {}",
+ login_response.loginStatus
+ ))),
+ }
+ }
+}
diff --git a/src/generated_methods.rs b/src/generated_methods.rs
index 0a5c646..6015951 100644
--- a/src/generated_methods.rs
+++ b/src/generated_methods.rs
@@ -8,7 +8,7 @@ use crate::generated_types::*;
use crate::json_rpc::RpcRequest;
use crate::result::Result;
use chrono::{DateTime, Utc};
-impl crate::BFClient {
+impl crate::client::BFClient {
#[allow(dead_code)]
pub fn listEventTypes(
&self,
diff --git a/src/lib.rs b/src/lib.rs
index 7a22055..442c37e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,234 +17,14 @@
#[macro_use]
extern crate log;
-use crate::json_rpc::{RpcRequest, RpcResponse};
-use crate::result::{Error, Result};
-use reqwest::{Client, Identity};
-use serde::de::DeserializeOwned;
-use serde::{Deserialize, Serialize};
-use std::sync::{Arc, RwLock};
-
+pub mod client;
mod generated_methods;
mod generated_requests;
pub mod generated_types;
mod json_rpc;
pub mod result;
-pub mod prelude {
- pub use crate::BFClient;
- pub use crate::BFCredentials;
-}
-
-#[derive(Debug, Serialize)]
-struct LoginRequestForm {
- username: String,
- password: String,
-}
-
-#[derive(Debug, Deserialize)]
-#[allow(non_snake_case)]
-struct LoginResponse {
- sessionToken: Option<String>,
- loginStatus: String, // TODO enum this
-}
-
-pub struct BFCredentials {
- username: String,
- password: String,
- pfx: Vec<u8>,
- app_key: String,
-}
-
-impl BFCredentials {
- pub fn new(
- username: String,
- password: String,
- pfx_path: String,
- app_key: String,
- ) -> Result<Self> {
- let pfx = std::fs::read(pfx_path)?;
- Ok(BFCredentials {
- username,
- password,
- pfx,
- app_key,
- })
- }
-
- fn as_login_request_form(&self) -> LoginRequestForm {
- LoginRequestForm {
- username: self.username.clone(),
- password: self.password.clone(),
- }
- }
- fn pfx(&self) -> &Vec<u8> {
- &self.pfx
- }
- fn app_key(&self) -> &String {
- &self.app_key
- }
-}
-
-pub struct BFClient {
- client: reqwest::Client,
- session_token: Arc<RwLock<Option<String>>>,
- creds: BFCredentials,
- proxy_uri: Option<String>,
-}
-
-impl BFClient {
- pub fn new(
- creds: BFCredentials,
- proxy_uri: Option<String>,
- ) -> Result<Self> {
- let client: reqwest::Client = match &proxy_uri {
- Some(uri) => {
- let proxy = reqwest::Proxy::all(uri)?;
- Client::builder().proxy(proxy).build()?
- }
- None => reqwest::Client::new(),
- };
- Ok(BFClient {
- client,
- session_token: Arc::new(RwLock::new(None)),
- creds,
- proxy_uri,
- })
- }
-
- // TODO keepalive
- // https://identitysso.betfair.com/api/keepAlive
- // Accept (mandatory)
- // Header that signals that the response should be returned as JSON application/json
- // X-Authentication (mandatory)
- // Header that represents the session token that needs to be keep alive Session Token
- // X-Application (optional)
- // Header the Application Key used by the customer to identify the product. App Key
- // Response structure
- //
- //
- // {
- // "token":"<token_passed_as_header>",
- // "product":"product_passed_as_header",
- // "status":"<status>",
- // "error":"<error>"
- // }
- // Status values
- //
- //
- // SUCCESS
- // FAIL
- // Error values
- //
- //
- // INPUT_VALIDATION_ERROR
- // INTERNAL_ERROR
- // NO_SESSION
-
- // general notes
- // We would therefore recommend that all Betfair API request are sent with the ‘Accept-Encoding: gzip, deflate’ request header.
- // We recommend that Connection: keep-alive header is set for all requests to guarantee a persistent connection and therefore reducing latency. Please note: Idle keep-alive connection to the API endpoints are closed every 3 minutes.
- // You should ensure that you handle the INVALID_SESSION_TOKEN error within your code by creating a new session token via the API login method.
- fn req_internal<T1: Serialize, T2: DeserializeOwned>(
- &self,
- maybe_token: &Option<String>,
- rpc_request: &RpcRequest<T1>,
- ) -> Result<RpcResponse<T2>> {
- match maybe_token {
- None => Err(Error::General(
- "req_internal: must login first".to_owned(),
- )),
- Some(token) => {
- const JSONRPC_URI: &str =
- "https://api.betfair.com/exchange/betting/json-rpc/v1";
-
- Ok(self
- .client
- .post(JSONRPC_URI)
- .header("X-Application", self.creds.app_key())
- .header("X-Authentication", token)
- .json(&rpc_request)
- .send()?
- .json()
- .unwrap())
- }
- }
- }
-
- /// Perform a request, logging in if necessary, fail if login
- pub fn req<T1: Serialize, T2: DeserializeOwned>(
- &self,
- req: RpcRequest<T1>,
- ) -> Result<RpcResponse<T2>> {
- // Initially acquire the token via a read lock
-
- trace!("Taking token read lock");
- let token_lock = self.session_token.read().unwrap();
- let mut token = token_lock.clone();
- drop(token_lock);
- trace!("Dropped token read lock");
-
- loop {
- // TODO: exponential backoff
-
- info!("Performing a request");
- match self.req_internal(&token, &req) {
- Ok(resp) => return Ok(resp),
- Err(_) => {
- info!("Not logged in");
- // Assume the only error possible is an auth error
-
- trace!("Taking token write lock");
- let mut token_lock = self.session_token.write().unwrap();
-
- if *token_lock == token {
- *token_lock = Some(self.login()?);
- }
- token = token_lock.clone();
-
- drop(token_lock); // drops at end of scope but we log
- trace!("Dropped token read lock");
- }
- }
- }
- }
-
- fn login(&self) -> Result<String> {
- const CERTLOGIN_URI: &str =
- "https://identitysso-cert.betfair.com/api/certlogin";
-
- let ident =
- Identity::from_pkcs12_der(self.creds.pfx().as_slice(), "")?;
-
- let client: reqwest::Client = match &(self.proxy_uri) {
- Some(uri) => {
- let proxy = reqwest::Proxy::all(uri)?;
- Client::builder().identity(ident).proxy(proxy).build()?
- }
- None => Client::builder().identity(ident).build()?,
- };
-
- let login_request_form = self.creds.as_login_request_form();
-
- info!("LoginRequest ...");
- let login_response: LoginResponse = client
- .post(CERTLOGIN_URI)
- .header(
- "X-Application",
- format!("schroedinger_{}", rand::random::<u128>()),
- )
- .form(&login_request_form)
- .send()?
- .json()?;
-
- info!("LoginResponse: {:?}", login_response.loginStatus);
-
- match login_response.sessionToken {
- Some(token) => Ok(token),
- None => Err(Error::BFLoginFailure(format!(
- "loginStatus: {}",
- login_response.loginStatus
- ))),
- }
- }
+pub mod prelude {
+ pub use crate::client::BFClient;
+ pub use crate::client::BFCredentials;
}