Skip to content

Commit

Permalink
Merge pull request #314 from bug-or-feature/invalid_token
Browse files Browse the repository at this point in the history
Better handling for invalid session token
  • Loading branch information
bug-or-feature authored Dec 2, 2023
2 parents f3263dd + 5ce8590 commit 8891c0d
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 24 deletions.
95 changes: 93 additions & 2 deletions tests/retry_test.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from trading_ig.rest import IGService, ApiExceededException
from trading_ig.rest import IGService, ApiExceededException, TokenInvalidException
import responses
from responses import Response
import json
import tenacity
from tenacity import Retrying
import pandas as pd

RETRYABLE = (ApiExceededException, TokenInvalidException)


class TestRetry:
# test_retry

@responses.activate
def test_retry(self):
def test_exceed_retry(self):
with open("tests/data/accounts_balances.json", "r") as file:
response_body = json.loads(file.read())

Expand Down Expand Up @@ -58,3 +60,92 @@ def test_retry(self):
assert responses.assert_call_count(url, 4) is True
assert isinstance(result, pd.DataFrame)
assert result.iloc[0]["accountId"] == "XYZ987"

@responses.activate
def test_token_retry(self):
with open("tests/data/accounts_balances.json", "r") as file:
response_body = json.loads(file.read())

url = "https://demo-api.ig.com/gateway/deal/accounts"
headers = {"CST": "abc123", "X-SECURITY-TOKEN": "xyz987"}
client_token_error = {"errorCode": "error.security.client-token-invalid"}
oauth_token_error = {"errorCode": "error.security.oauth-token-invalid"}

responses.add(
Response(
method="GET", url=url, headers={}, json=client_token_error, status=403
)
)
responses.add(
Response(
method="GET", url=url, headers={}, json=oauth_token_error, status=403
)
)
responses.add(
Response(
method="GET", url=url, headers={}, json=client_token_error, status=403
)
)
responses.add(
Response(
method="GET", url=url, headers=headers, json=response_body, status=200
)
)

ig_service = IGService(
"username",
"password",
"api_key",
"DEMO",
retryer=Retrying(
wait=tenacity.wait_exponential(),
retry=tenacity.retry_if_exception_type(TokenInvalidException),
),
)

result = ig_service.fetch_accounts()

assert responses.assert_call_count(url, 4) is True
assert isinstance(result, pd.DataFrame)
assert result.iloc[0]["accountId"] == "XYZ987"

@responses.activate
def test_all_retry(self):
with open("tests/data/accounts_balances.json", "r") as file:
response_body = json.loads(file.read())

url = "https://demo-api.ig.com/gateway/deal/accounts"
headers = {"CST": "abc123", "X-SECURITY-TOKEN": "xyz987"}
client_token_error = {"errorCode": "error.security.client-token-invalid"}
api_exceeded = {"errorCode": "error.public-api.exceeded-api-key-allowance"}

responses.add(
Response(
method="GET", url=url, headers={}, json=client_token_error, status=403
)
)
responses.add(
Response(method="GET", url=url, headers={}, json=api_exceeded, status=403)
)
responses.add(
Response(
method="GET", url=url, headers=headers, json=response_body, status=200
)
)

ig_service = IGService(
"username",
"password",
"api_key",
"DEMO",
retryer=Retrying(
wait=tenacity.wait_exponential(),
retry=tenacity.retry_if_exception_type(RETRYABLE),
),
)

result = ig_service.fetch_accounts()

assert responses.assert_call_count(url, 3) is True
assert isinstance(result, pd.DataFrame)
assert result.iloc[0]["accountId"] == "XYZ987"
13 changes: 11 additions & 2 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from trading_ig.rest import IGService, IGException, ApiExceededException
from trading_ig.rest import (
IGService,
IGException,
ApiExceededException,
TokenInvalidException,
)
from trading_ig.config import config
import pandas as pd
from datetime import datetime, timedelta
Expand All @@ -9,11 +14,15 @@
from tenacity import Retrying, wait_exponential, retry_if_exception_type


RETRYABLE = (ApiExceededException, TokenInvalidException)


@pytest.fixture(scope="module")
def retrying():
"""test fixture creates a tenacity.Retrying instance"""
return Retrying(
wait=wait_exponential(), retry=retry_if_exception_type(ApiExceededException)
wait=wait_exponential(),
retry=retry_if_exception_type(RETRYABLE),
)


Expand Down
36 changes: 16 additions & 20 deletions trading_ig/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@

from datetime import timedelta, datetime
from .utils import _HAS_PANDAS, _HAS_MUNCH
from .utils import conv_resol, conv_datetime, conv_to_ms, DATE_FORMATS
from .utils import (
conv_resol,
conv_datetime,
conv_to_ms,
DATE_FORMATS,
api_limit_hit,
token_invalid,
)

if _HAS_MUNCH:
from .utils import munchify
Expand All @@ -42,7 +49,7 @@ class ApiExceededException(Exception):


class TokenInvalidException(Exception):
"""Raised when the v3 session token is invalid or expired"""
"""Raised when the session token is invalid or expired"""

pass

Expand Down Expand Up @@ -101,8 +108,11 @@ def create(self, endpoint, params, session, version):
response = session.post(url, data=json.dumps(params))
logger.info(f"POST '{endpoint}', resp {response.status_code}")
if response.status_code in [401, 403]:
if "exceeded-api-key-allowance" in response.text:
if api_limit_hit(response.text):
raise ApiExceededException()
if token_invalid(response.text):
logger.warning("Invalid session token, triggering refresh...")
raise TokenInvalidException()
if "error.public-api.failure.kyc.required" in response.text:
raise KycRequiredException(
"KYC issue: you need to login manually to the web interface and "
Expand Down Expand Up @@ -389,28 +399,14 @@ def _request(self, action, endpoint, params, session, version="1", check=True):
)

response.encoding = "utf-8"
if self._api_limit_hit(response.text):
if api_limit_hit(response.text):
raise ApiExceededException()
if self._token_invalid(response.text):
logger.error("Invalid authentication token, triggering refresh...")
if token_invalid(response.text):
logger.warning("Invalid session token, triggering refresh...")
self._valid_until = datetime.now() - timedelta(seconds=15)
raise TokenInvalidException()
return response

@staticmethod
def _api_limit_hit(response_text):
# note we don't check for historical data allowance - it only gets reset
# once a week
return (
"exceeded-api-key-allowance" in response_text
or "exceeded-account-allowance" in response_text
or "exceeded-account-trading-allowance" in response_text
)

@staticmethod
def _token_invalid(response_text):
return "oauth-token-invalid" in response_text

# ---------- PARSE_RESPONSE ----------- #

@staticmethod
Expand Down
17 changes: 17 additions & 0 deletions trading_ig/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,20 @@ def print_full(x):
pd.reset_option("display.width")
pd.reset_option("display.float_format")
pd.reset_option("display.max_colwidth")


def api_limit_hit(response_text):
# note we don't check for historical data allowance - it only gets reset
# once a week
return (
"exceeded-api-key-allowance" in response_text
or "exceeded-account-allowance" in response_text
or "exceeded-account-trading-allowance" in response_text
)


def token_invalid(response_text):
return (
"oauth-token-invalid" in response_text
or "client-token-invalid" in response_text
)

0 comments on commit 8891c0d

Please sign in to comment.