aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Edgecumbe <git@esotericnonsense.com>2019-10-29 00:48:02 +0100
committerDaniel Edgecumbe <git@esotericnonsense.com>2019-10-29 00:48:04 +0100
commit5f80ee86f3e3153c865b97c9a247fe17047531b1 (patch)
tree88c0cddcb6aab8745d2ac9962b293143da76bb95
parent213af9f9a7990e94a9fa14ca80d2f3fd5afd56bb (diff)
Handle login required errors correctly
We handle three specific cases here: the two APINGExceptions, and the more generic 'InvalidHeaderValue' response. There may be other errors that could occur that would require re-login, but we can worry about those if they crop up.
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml2
-rw-r--r--TODO.md4
-rwxr-xr-xgenapi/main.py4
-rw-r--r--src/client.rs105
-rw-r--r--src/generated_methods.rs52
-rw-r--r--src/json_rpc.rs16
-rw-r--r--src/result.rs3
8 files changed, 122 insertions, 65 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4c39371..4c29d51 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -64,6 +64,7 @@ name = "botfair"
version = "0.3.99"
dependencies = [
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.22 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/Cargo.toml b/Cargo.toml
index 76f9634..e79f2bc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,3 +25,5 @@ serde = "1"
# Some of the BF datatypes require datetimes.
chrono = { "version" = "0.4", "features" = ["serde"] }
+
+http = "0.1"
diff --git a/TODO.md b/TODO.md
index 57957d6..45eea1b 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,2 +1,4 @@
if all struct fields are optional, derive default
-parse exceptions
+builder macros?
+parse exceptions more fully
+cache the session tokens across multiple instantiations
diff --git a/genapi/main.py b/genapi/main.py
index 189a8e8..8c146db 100755
--- a/genapi/main.py
+++ b/genapi/main.py
@@ -649,7 +649,7 @@ let rpc_request: RpcRequest<{struct_name}> = RpcRequest::new(
\"SportsAPING/v1.0/{operation.name}\".to_owned(),
req
);
-self.req(rpc_request).map(|x| x.into_inner())?
+self.req(rpc_request)
"""
else:
# TODO this smells, repetition
@@ -658,7 +658,7 @@ let rpc_request: RpcRequest<()> = RpcRequest::new(
\"SportsAPING/v1.0/{operation.name}\".to_owned(),
()
);
-self.req(rpc_request).map(|x| x.into_inner())?
+self.req(rpc_request)
"""
function_signature = f"""fn {operation.name}({formatted_params_args}) ->
diff --git a/src/client.rs b/src/client.rs
index 68a3790..25ea157 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -14,6 +14,7 @@
// 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::generated_exceptions::errorCode;
use crate::json_rpc::{RpcRequest, RpcResponse};
use crate::result::{Error, Result};
use reqwest::{Client, Identity};
@@ -187,26 +188,76 @@ impl BFClient {
&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";
-
- trace!("Performing a query to the JSON-RPC api");
-
- Ok(self
- .client
- .post(JSONRPC_URI)
- .header("X-Application", self.creds.app_key())
- .header("X-Authentication", token)
- .json(&rpc_request)
- .send()?
- .json()
- .unwrap())
+ ) -> Result<T2> {
+ let token = match maybe_token {
+ Some(x) => x,
+ None => return Err(Error::SessionTokenNotPresent),
+ };
+
+ const JSONRPC_URI: &str =
+ "https://api.betfair.com/exchange/betting/json-rpc/v1";
+
+ trace!("Performing a query to the JSON-RPC api");
+
+ // Attempt request
+ let mut http_response: reqwest::Response = {
+ let maybe_http_response = self
+ .client
+ .post(JSONRPC_URI)
+ .header("X-Application", self.creds.app_key())
+ .header("X-Authentication", token)
+ .json(&rpc_request)
+ .send();
+
+ match maybe_http_response {
+ Ok(x) => x,
+ Err(e) => {
+ match e
+ .get_ref()
+ .and_then(|f| f.downcast_ref::<http::Error>())
+ .and_then(|g| {
+ Some(g.is::<http::header::InvalidHeaderValue>())
+ }) {
+ Some(true) => {
+ // This error occurs if you pass a random
+ // string in the authentication header.
+ debug!("req_internal: InvalidHeaderValue");
+ return Err(Error::SessionTokenInvalid);
+ }
+ _ => {
+ error!("req_internal: request error {}", e);
+ return Err(Error::Reqwest(e));
+ }
+ }
+ }
+ }
+ };
+
+ // Attempt to deserialize
+ let rpc_response: RpcResponse<T2> = match http_response.json() {
+ Ok(x) => x,
+ Err(e) => {
+ error!("req_internal: deserialization error {}", e);
+ return Err(Error::Reqwest(e));
+ }
+ };
+
+ match rpc_response.into_inner() {
+ Ok(x) => Ok(x),
+ Err(Error::APINGException(code)) => match code {
+ errorCode::INVALID_SESSION_INFORMATION
+ | errorCode::NO_SESSION => Err(Error::SessionTokenInvalid),
+ e => {
+ error!("req_internal: API error {:?}", e);
+ Err(Error::APINGException(e))
+ }
+ },
+ Err(Error::JSONRPCError) => {
+ error!("req_internal: no result or error?");
+ Err(Error::JSONRPCError)
+ }
+ Err(_) => {
+ unreachable!();
}
}
}
@@ -214,7 +265,7 @@ impl BFClient {
pub(super) fn req<T1: Serialize, T2: DeserializeOwned>(
&self,
req: RpcRequest<T1>,
- ) -> Result<RpcResponse<T2>> {
+ ) -> Result<T2> {
// Initially acquire the token via a read lock
trace!("req: taking token read lock");
@@ -230,12 +281,8 @@ impl BFClient {
debug!("req: request successful");
break Ok(resp);
}
- Err(_) => {
- // Assume the only error possible is an auth error
- // TODO: check if it's an exception, auth error,
- // etc; an exception should just be propagated to the
- // caller
-
+ Err(Error::SessionTokenNotPresent)
+ | Err(Error::SessionTokenInvalid) => {
info!("req: login required");
trace!("req: taking token write lock");
let mut token_lock = self.session_token.write().unwrap();
@@ -267,6 +314,10 @@ impl BFClient {
drop(token_lock); // explicit drop for logging purposes
trace!("req: dropped token write lock");
}
+ Err(e) => {
+ error!("req: unhandled error {:?}", e);
+ break Err(e);
+ }
}
}
}
diff --git a/src/generated_methods.rs b/src/generated_methods.rs
index edc9bd1..ba2f723 100644
--- a/src/generated_methods.rs
+++ b/src/generated_methods.rs
@@ -24,7 +24,7 @@ impl crate::client::BFClient {
listEventTypesRequest { filter, locale };
let rpc_request: RpcRequest<listEventTypesRequest> =
RpcRequest::new("SportsAPING/v1.0/listEventTypes".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of Competitions (i.e., World Cup 2013) associated with the markets selected by the MarketFilter. Currently only Football markets have an associated competition.
#[allow(dead_code)]
@@ -39,7 +39,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/listCompetitions".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of time ranges in the granularity specified in the request (i.e. 3PM to 4PM, Aug 14th to Aug 15th) associated with the markets selected by the MarketFilter.
#[allow(dead_code)]
@@ -54,7 +54,7 @@ impl crate::client::BFClient {
};
let rpc_request: RpcRequest<listTimeRangesRequest> =
RpcRequest::new("SportsAPING/v1.0/listTimeRanges".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of Events (i.e, Reading vs. Man United) associated with the markets selected by the MarketFilter.
#[allow(dead_code)]
@@ -66,7 +66,7 @@ impl crate::client::BFClient {
let req: listEventsRequest = listEventsRequest { filter, locale };
let rpc_request: RpcRequest<listEventsRequest> =
RpcRequest::new("SportsAPING/v1.0/listEvents".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of market types (i.e. MATCH_ODDS, NEXT_GOAL) associated with the markets selected by the MarketFilter. The market types are always the same, regardless of locale.
#[allow(dead_code)]
@@ -81,7 +81,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/listMarketTypes".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of Countries associated with the markets selected by the MarketFilter.
#[allow(dead_code)]
@@ -94,7 +94,7 @@ impl crate::client::BFClient {
listCountriesRequest { filter, locale };
let rpc_request: RpcRequest<listCountriesRequest> =
RpcRequest::new("SportsAPING/v1.0/listCountries".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of Venues (i.e. Cheltenham, Ascot) associated with the markets selected by the MarketFilter. Currently, only Horse Racing markets are associated with a Venue.
#[allow(dead_code)]
@@ -106,7 +106,7 @@ impl crate::client::BFClient {
let req: listVenuesRequest = listVenuesRequest { filter, locale };
let rpc_request: RpcRequest<listVenuesRequest> =
RpcRequest::new("SportsAPING/v1.0/listVenues".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of information about markets that does not change (or changes very rarely). You use listMarketCatalogue to retrieve the name of the market, the names of selections and other information about markets.
#[allow(dead_code)]
@@ -130,7 +130,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/listMarketCatalogue".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of dynamic data about markets. Dynamic data includes prices, the status of the market, the status of selections, the traded volume, and the status of any orders you have placed in the market.
#[allow(dead_code)]
@@ -163,7 +163,7 @@ impl crate::client::BFClient {
};
let rpc_request: RpcRequest<listMarketBookRequest> =
RpcRequest::new("SportsAPING/v1.0/listMarketBook".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of dynamic data about a market and a specified runner. Dynamic data includes prices, the status of the market, the status of selections, the traded volume, and the status of any orders you have placed in the market.
#[allow(dead_code)]
@@ -200,7 +200,7 @@ impl crate::client::BFClient {
};
let rpc_request: RpcRequest<listRunnerBookRequest> =
RpcRequest::new("SportsAPING/v1.0/listRunnerBook".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a list of your current orders. Optionally you can filter and sort your current orders using the various parameters, setting none of the parameters will return all of your current orders, up to a maximum of 1000 bets, ordered BY_BET and sorted EARLIEST_TO_LATEST. To retrieve more than 1000 orders, you need to make use of the fromRecord and recordCount parameters.
#[allow(dead_code)]
@@ -236,7 +236,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/listCurrentOrders".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Returns a List of bets based on the bet status, ordered by settled date
#[allow(dead_code)]
@@ -280,7 +280,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/listClearedOrders".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Place new orders into market. LIMIT orders below the minimum bet size are allowed if there is an unmatched bet at the same price in the market. This operation is atomic in that all orders will be placed or none will be placed.
#[allow(dead_code)]
@@ -303,7 +303,7 @@ impl crate::client::BFClient {
};
let rpc_request: RpcRequest<placeOrdersRequest> =
RpcRequest::new("SportsAPING/v1.0/placeOrders".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Cancel all bets OR cancel all bets on a market OR fully or partially cancel particular orders on a market. Only LIMIT orders an be cancelled or partially cancelled once placed.
#[allow(dead_code)]
@@ -320,7 +320,7 @@ impl crate::client::BFClient {
};
let rpc_request: RpcRequest<cancelOrdersRequest> =
RpcRequest::new("SportsAPING/v1.0/cancelOrders".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// This operation is logically a bulk cancel followed by a bulk place. The cancel is completed first then the new orders are placed. The new orders will be placed atomically in that they will all be placed or none will be placed. In the case where the new orders cannot be placed the cancellations will not be rolled back. See ReplaceInstruction.
#[allow(dead_code)]
@@ -341,7 +341,7 @@ impl crate::client::BFClient {
};
let rpc_request: RpcRequest<replaceOrdersRequest> =
RpcRequest::new("SportsAPING/v1.0/replaceOrders".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Update non-exposure changing fields
#[allow(dead_code)]
@@ -358,7 +358,7 @@ impl crate::client::BFClient {
};
let rpc_request: RpcRequest<updateOrdersRequest> =
RpcRequest::new("SportsAPING/v1.0/updateOrders".to_owned(), req);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Retrieve profit and loss for a given list of markets. The values are calculated using matched bets and optionally settled bets. Only odds markets are implemented, markets of other types are silently ignored.
#[allow(dead_code)]
@@ -381,7 +381,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/listMarketProfitAndLoss".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Create/update default exposure limit for market groups of given type. New value and breach action will be immediately applied to existing instances of this type (unless overridden using setExposureLimitForMarketGroup). If default values are overridden for market groups (using setExposureLimitForMarketGroup), overrides will NOT be touched. In order to clear this limit "removeDefaultExposureLimitForMarketGroups" operation should be used. It's not allowed to set default limit to an empty limit (see type ExposureLimit).
#[allow(dead_code)]
@@ -402,7 +402,7 @@ impl crate::client::BFClient {
.to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Create/update exposure limit for a market group. New limit will be applied immediately (even if a default limit exists for this type). The limit will be deleted upon account action (see deleteMarketGroupExposureLimit) or when no active markets remain under market group. It is possible to "invalidate" default limit for a specific group by using a "empty" limit (see type ExposureLimit). Upon successful execution of the request, the effective limit for this group will be the one set by this request (Properties will NOT be inherited from default limit).
#[allow(dead_code)]
@@ -418,7 +418,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/setExposureLimitForMarketGroup".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Remove default exposure limit for a market group type. This operation will NOT remove/update any market group limits.
#[allow(dead_code)]
@@ -437,7 +437,7 @@ impl crate::client::BFClient {
.to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Delete exposure limit for a market group. If a default exposure limit exist for market type, it takes effect immediately.
#[allow(dead_code)]
@@ -453,7 +453,7 @@ impl crate::client::BFClient {
.to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Response to this request returns default group limit and group limits grouped by type. If marketGroupTypeFilter is not populated values for all types are returned. The response will always contain the default limit. It is possible to control which groups to return using marketGroupsFilter parameter. If marketGroupsFilter is not set all group limits are returned. If an emtpy list is passed only default limit(s) is returned. When marketGroupTypeFilter and marketGroupsFilter used together, all groups in marketGroupsFilter are required to be of same type (type used in marketGroupTypeFilter).
#[allow(dead_code)]
@@ -473,7 +473,7 @@ impl crate::client::BFClient {
.to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Unblock a market group after it has been blocked due to the breach of a previously set exposure limit.
#[allow(dead_code)]
@@ -488,7 +488,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/unblockMarketGroup".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Retrieves events from exposure reuse enabled events list. To edit this list use addExposureReuseEnabledEvents and removeExposureReuseEnabledEvents operations.
#[allow(dead_code)]
@@ -497,7 +497,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/getExposureReuseEnabledEvents".to_owned(),
(),
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Enables events for exposure reuse by appending them to the current list of events already enabled.
#[allow(dead_code)]
@@ -512,7 +512,7 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/addExposureReuseEnabledEvents".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
/// Removes events from exposure reuse enabled events list.
#[allow(dead_code)]
@@ -527,6 +527,6 @@ impl crate::client::BFClient {
"SportsAPING/v1.0/removeExposureReuseEnabledEvents".to_owned(),
req,
);
- self.req(rpc_request).map(|x| x.into_inner())?
+ self.req(rpc_request)
}
}
diff --git a/src/json_rpc.rs b/src/json_rpc.rs
index b50623a..7e862d8 100644
--- a/src/json_rpc.rs
+++ b/src/json_rpc.rs
@@ -40,7 +40,7 @@ impl<T> RpcRequest<T> {
#[derive(Debug, Deserialize)]
pub struct RpcError {
- code: i32,
+ code: i32, // TODO are these ever meaningful?
message: errorCode,
}
@@ -48,7 +48,6 @@ pub struct RpcError {
pub struct RpcResponse<T> {
jsonrpc: String,
result: Option<T>,
- // TODO custom serde deserializer?
error: Option<RpcError>,
id: String,
}
@@ -56,15 +55,14 @@ pub struct RpcResponse<T> {
impl<T> RpcResponse<T> {
// TODO: rustic way to perform this?
pub fn into_inner(self) -> Result<T> {
- // TODO check these? do we care?
- let _ = self.jsonrpc;
- let _ = self.id;
- match self.error {
- Some(rpc_error) => {
- // parse out the error. ANGX-0001
+ let _ = self.jsonrpc; // This should always be "2.0".
+ let _ = self.id; // We could check this against the request.
+ match (self.error, self.result) {
+ (Some(rpc_error), _) => {
Err(Error::APINGException(rpc_error.message))
}
- None => Ok(self.result.expect("unhandled API exception")),
+ (None, Some(result)) => Ok(result),
+ (None, None) => Err(Error::JSONRPCError),
}
}
}
diff --git a/src/result.rs b/src/result.rs
index ab5d6b8..cb4791b 100644
--- a/src/result.rs
+++ b/src/result.rs
@@ -24,6 +24,9 @@ pub enum Error {
BFLoginFailure(String),
BFKeepAliveFailure(crate::client::KeepAliveError), // could be an enum
General(String),
+ JSONRPCError,
+ SessionTokenNotPresent,
+ SessionTokenInvalid,
Other,
}