Skip to content

Commit

Permalink
Resolved pylint error.
Browse files Browse the repository at this point in the history
  • Loading branch information
prijendev committed Sep 12, 2022
1 parent 805c830 commit 042885a
Showing 1 changed file with 211 additions and 26 deletions.
237 changes: 211 additions & 26 deletions tap_freshdesk/client.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,248 @@
import time

import backoff
import requests
import singer
import pendulum
from singer import utils

import singer
import time

LOGGER = singer.get_logger()
BASE_URL = "https://{}.freshdesk.com"

BASE_ID_URL = "https://id.getharvest.com/api/v2/"
BASE_API_URL = "https://api.harvestapp.com/v2/"
# timeout request after 300 seconds
REQUEST_TIMEOUT = 300

class HarvestError(Exception):
pass

class Server5xxError(Exception):
pass

class HarvestBadRequestError(HarvestError):
pass

class HarvestUnauthorizedError(HarvestError):
pass

class HarvestNotFoundError(HarvestError):
pass

class HarvestForbiddenError(HarvestError):
pass

class HarvestUnprocessableEntityError(HarvestError):
pass

class HarvestRateLimitExceeededError(HarvestError):
pass

class FreshdeskClient:
class HarvestInternalServiceError(Server5xxError):
pass

ERROR_CODE_EXCEPTION_MAPPING = {
400: {
"raise_exception": HarvestBadRequestError,
"message": "The request is missing or has a bad parameter."
},
401: {
"raise_exception": HarvestUnauthorizedError,
"message": "Invalid authorization credentials."
},
403: {
"raise_exception": HarvestForbiddenError,
"message": "User does not have permission to access the resource or "\
"related feature is disabled."
},
404: {
"raise_exception": HarvestNotFoundError,
"message": "The resource you have specified cannot be found."
},
422: {
"raise_exception": HarvestUnprocessableEntityError,
"message": "The request was not able to process right now."
},
429: {
"raise_exception": HarvestRateLimitExceeededError,
"message": "API rate limit exceeded."
},
500: {
"raise_exception": HarvestInternalServiceError,
"message": "An error has occurred at Harvest's end."
}
}

def raise_for_error(response):
"""
The client class is used for making REST calls to the Freshdesk API.
Forming a custom response message for raising an exception.
"""

error_code = response.status_code
try:
response_json = response.json()
except Exception:
response_json = {}

if error_code not in ERROR_CODE_EXCEPTION_MAPPING and error_code > 500:
# Raise `Server5xxError` for all 5xx unknown error
exc = Server5xxError
else:
exc = ERROR_CODE_EXCEPTION_MAPPING.get(error_code, {}).get("raise_exception", HarvestError)
error_message = response_json.get("error_description", ERROR_CODE_EXCEPTION_MAPPING.get(
error_code, {}).get("message", "An Unknown Error occurred."))
message = "HTTP-error-code: {}, Error: {}".format(error_code, error_message)

raise exc(message) from None

class HarvestClient: #pylint: disable=too-many-instance-attributes
"""
The client class is used for making REST calls to the Harvest API.
"""

def __init__(self, config):
self.config = config
self._client_id = config['client_id']
self._client_secret = config['client_secret']
self._refresh_token = config['refresh_token']
self._user_agent = config['user_agent']
self._account_id = None
self.session = requests.Session()
self.base_url = BASE_URL.format(config.get("domain"))
self._access_token = None
self._expires_at = None
self.request_timeout = self.get_request_timeout()

def __enter__(self):
self.check_access_token()
return self
self._refresh_access_token()

def __exit__(self, exception_type, exception_value, traceback):
# Kill the session instance.
self.session.close()

def check_access_token(self):
def get_request_timeout(self):
"""
Check if the access token is valid.
Get timeout value from config, if the value is passed.
Else return the default value.
"""
self.request(self.base_url+"/api/v2/roles", {"per_page": 1, "page": 1})
# Get `request_timeout` value from config.
config_request_timeout = self.config.get('request_timeout')

# If timeout is not passed in the config then set it to the default(300 seconds)
if config_request_timeout is None:
return REQUEST_TIMEOUT

# If config request_timeout is other than 0,"0" or invalid string then use request_timeout
if ((type(config_request_timeout) in [int, float]) or
(isinstance(config_request_timeout,str) and config_request_timeout.replace('.', '', 1).isdigit())) and float(config_request_timeout):
return float(config_request_timeout)
raise Exception("The entered timeout is invalid, it should be a valid none-zero integer.")

@backoff.on_exception(backoff.expo,
(requests.exceptions.RequestException),
(HarvestRateLimitExceeededError, Server5xxError,
requests.Timeout, requests.ConnectionError),
max_tries=5,
giveup=lambda e: e.response is not None and 400 <= e.response.status_code < 500,
factor=2)
@utils.ratelimit(1, 2)
def request(self, url, params=None):
def _refresh_access_token(self):
"""
Call rest API and return the response in case of status code 200.
Create an access token using the refresh token.
"""
headers = {}
if 'user_agent' in self.config:
headers['User-Agent'] = self.config['user_agent']
LOGGER.info("Refreshing access token")
resp = self.session.request('POST',
url=BASE_ID_URL + 'oauth2/token',
data={
'client_id': self._client_id,
'client_secret': self._client_secret,
'refresh_token': self._refresh_token,
'grant_type': 'refresh_token',
},
headers={"User-Agent": self._user_agent})

req = requests.Request('GET', url, params=params, auth=(self.config['api_key'], ""), headers=headers).prepare()
LOGGER.info("GET %s", req.url)
response = self.session.send(req)
expires_in_seconds = resp.json().get('expires_in', 17 * 60 * 60)
self._expires_at = pendulum.now().add(seconds=expires_in_seconds)
resp_json = {}
try:
resp_json = resp.json()
self._access_token = resp_json['access_token']
# If an access token is not provided in response, raise an error
except KeyError:
if resp_json.get('error'):
LOGGER.critical(resp_json.get('error'))
if resp_json.get('error_description'):
LOGGER.critical(resp_json.get('error_description'))
raise_for_error(resp)
LOGGER.info("Got refreshed access token")

def get_access_token(self):
"""
Return access token if available or generate one.
"""
if self._access_token is not None and self._expires_at > pendulum.now():
return self._access_token

self._refresh_access_token()
return self._access_token

@backoff.on_exception(backoff.expo,
(HarvestRateLimitExceeededError, Server5xxError,
requests.Timeout, requests.ConnectionError),
max_tries=5,
factor=2)
def get_account_id(self):
"""
Get the account Id of the Active Harvest account.
It will throw an exception if no active harvest account is found.
"""
if self._account_id is not None:
return self._account_id

response = self.session.request('GET',
url=BASE_ID_URL + 'accounts',
headers={'Authorization': 'Bearer ' + self._access_token,
'User-Agent': self._user_agent},
timeout=self.request_timeout)

# Call the function again if the rate limit is exceeded
if 'Retry-After' in response.headers:
retry_after = int(response.headers['Retry-After'])
LOGGER.info("Rate limit reached. Sleeping for %s seconds", retry_after)
time.sleep(retry_after)
return self.get_account_id()

if response.status_code != 200:
raise_for_error(response)

if response.json().get('accounts'):
self._account_id = str(response.json()['accounts'][0]['id'])
return self._account_id

raise Exception("No Active Harvest Account found") from None

@backoff.on_exception(backoff.expo,
(HarvestRateLimitExceeededError, Server5xxError,
requests.Timeout, requests.ConnectionError),
max_tries=5,
factor=2)
@utils.ratelimit(100, 15)
def request(self, url, params=None):
"""
Call rest API and return the response in case of status code 200.
"""
params = params or {}
access_token = self.get_access_token()
headers = {"Accept": "application/json",
"Harvest-Account-Id": self.get_account_id(),
"Authorization": "Bearer " + access_token,
"User-Agent": self._user_agent}
req = requests.Request("GET", url=url, params=params, headers=headers).prepare()
LOGGER.info("GET %s", req.url)
resp = self.session.send(req, timeout=self.request_timeout)

# Call the function again if the rate limit is exceeded
if 'Retry-After' in resp.headers:
retry_after = int(resp.headers['Retry-After'])
LOGGER.info("Rate limit reached. Sleeping for %s seconds", retry_after)
time.sleep(retry_after)
return self.request(url, params)

response.raise_for_status()
if resp.status_code != 200:
raise_for_error(resp)

return response.json()
return resp.json()

0 comments on commit 042885a

Please sign in to comment.