Add logging everywhere
This commit is contained in:
parent
5b4523c797
commit
2deb99c51c
5 changed files with 124 additions and 25 deletions
|
@ -1,4 +1 @@
|
|||
from .re2oapi.client import Re2oAPIClient
|
||||
from .re2oapi import exceptions
|
||||
|
||||
__all__ = ['Re2oAPIClient', 'exceptions']
|
||||
from .re2oapi import *
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
from .client import Re2oAPIClient
|
||||
from . import exceptions
|
||||
|
||||
__all__ = ['Re2oAPIClient', 'exceptions']
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
import datetime
|
||||
import iso8601
|
||||
from pathlib import Path
|
||||
|
@ -23,7 +24,8 @@ class Re2oAPIClient:
|
|||
process for you.
|
||||
"""
|
||||
|
||||
def __init__(self, hostname, username, password, token_file=None, use_tls=True):
|
||||
def __init__(self, hostname, username, password, token_file=None,
|
||||
use_tls=True, log_level=logging.CRITICAL+10):
|
||||
"""Creates an API client.
|
||||
|
||||
Args:
|
||||
|
@ -36,7 +38,9 @@ class Re2oAPIClient:
|
|||
default value is `None`, which indicated to use
|
||||
`$HOME/{DEFAULT_TOKEN_FILENAME}`.
|
||||
use_tls: A boolean to indicate whether the client should us TLS
|
||||
(recommended for production). The Default is `True`.
|
||||
(recommended for production). The default is `True`.
|
||||
log_level: Control the logging level to use. The default is
|
||||
`logging.CRITICAL+10`. So nothing is logged.
|
||||
|
||||
Raises:
|
||||
requests.exceptions.ConnectionError: Unable to resolve the
|
||||
|
@ -47,6 +51,24 @@ class Re2oAPIClient:
|
|||
are not valid according to the Re2o server.
|
||||
""".format(DEFAULT_TOKEN_FILENAME=DEFAULT_TOKEN_FILENAME)
|
||||
|
||||
# Enable logging
|
||||
self.log = logging.getLogger(__name__)
|
||||
if not self.log.hasHandlers():
|
||||
# Avoid multiplicating handlers
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s %(name)s %(message)s"
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
self.log.addHandler(handler)
|
||||
self.log.setLevel(log_level)
|
||||
|
||||
self.log.info("Starting new Re2o API client.")
|
||||
self.log.debug("hostname = " + str(hostname))
|
||||
self.log.debug("username = " + str(username))
|
||||
self.log.debug("token_file = " + str(token_file))
|
||||
self.log.debug("use_tls = " + str(use_tls))
|
||||
|
||||
self.use_tls = use_tls
|
||||
self.token_file = token_file or Path.home() / DEFAULT_TOKEN_FILENAME
|
||||
self.hostname = hostname
|
||||
|
@ -73,29 +95,47 @@ class Re2oAPIClient:
|
|||
datetime.timedelta(seconds=TIME_FOR_RENEW)
|
||||
|
||||
def _get_token_from_file(self):
|
||||
self.log.debug("Retrieving token from token file '{}'."
|
||||
.format(self.token_file))
|
||||
|
||||
# Check the token file exists
|
||||
if not self.token_file.is_file():
|
||||
raise exceptions.TokenFileNotFound(self.token_file)
|
||||
e = exceptions.TokenFileNotFound(self.token_file)
|
||||
self.log.error(e)
|
||||
raise e
|
||||
|
||||
# Read the data in the file
|
||||
try:
|
||||
with self.token_file.open() as f:
|
||||
data = json.load(f)
|
||||
except Exception:
|
||||
e = exceptions.TokenFileNotReadable(self.token_file)
|
||||
self.log.error(e)
|
||||
raise e
|
||||
|
||||
try:
|
||||
# Retrieve data for this hostname and this username in the file
|
||||
token_data = data[self.hostname][self._username]
|
||||
return {
|
||||
ret = {
|
||||
'token': token_data['token'],
|
||||
'expiration': iso8601.parse_date(token_data['expiration'])
|
||||
}
|
||||
except KeyError:
|
||||
raise exceptions.TokenNotInTokenFile(
|
||||
e = exceptions.TokenNotInTokenFile(
|
||||
self._username,
|
||||
self.hostname,
|
||||
self.token_file
|
||||
)
|
||||
self.log.error(e)
|
||||
raise e
|
||||
else:
|
||||
self.log.debug("Token successfully retrieved from token "
|
||||
"file '{}'.".format(self.token_file))
|
||||
return ret
|
||||
|
||||
def _save_token_to_file(self):
|
||||
self.log.debug("Saving token to token file '{}'."
|
||||
.format(self.token_file))
|
||||
|
||||
try:
|
||||
# Read previous data to not erase other tokens
|
||||
|
@ -103,6 +143,8 @@ class Re2oAPIClient:
|
|||
data = json.load(f)
|
||||
except Exception:
|
||||
# Default value if file not readbale or data not valid JSON
|
||||
self.log.warning("Token file '{}' is not a valid JSON readable "
|
||||
"file. Considered empty.".format(self.token_file))
|
||||
data = {}
|
||||
|
||||
# Insert the new token in the data (replace old token if it exists)
|
||||
|
@ -120,25 +162,38 @@ class Re2oAPIClient:
|
|||
json.dump(data, f)
|
||||
self.token_file.chmod(stat.S_IWRITE | stat.S_IREAD)
|
||||
except Exception:
|
||||
pass
|
||||
self.log.error("Token file '{}' could not be written. Passing."
|
||||
.format(self.token_file))
|
||||
else:
|
||||
self.log.debug("Token sucessfully writen in token file '{}'."
|
||||
.format(self.token_file))
|
||||
|
||||
def _get_token_from_server(self):
|
||||
self.log.debug("Requesting a new token form server '{}' for user "
|
||||
"'{}'.".format(self.hostname, self._username))
|
||||
|
||||
# Perform the authentication request
|
||||
response = requests.post(
|
||||
self.get_url_for('token'),
|
||||
data={'username': self._username, 'password': self._password}
|
||||
)
|
||||
self.log.debug("Response code: "+str(response.status_code))
|
||||
|
||||
if response.status_code == requests.codes.bad_request:
|
||||
raise exceptions.InvalidCredentials(self._username, self.hostname)
|
||||
e = exceptions.InvalidCredentials(self._username, self.hostname)
|
||||
self.log.error(e)
|
||||
raise e
|
||||
response.raise_for_status()
|
||||
|
||||
# Return the token and expiration time
|
||||
response = response.json()
|
||||
return {
|
||||
ret = {
|
||||
'token': response['token'],
|
||||
'expiration': iso8601.parse_date(response['expiration'])
|
||||
}
|
||||
self.log.debug("Token successfully retrieved from server '{}'."
|
||||
.format(self.hostname))
|
||||
return ret
|
||||
|
||||
def _force_renew_token(self):
|
||||
self.token = self._get_token_from_server()
|
||||
|
@ -161,35 +216,51 @@ class Re2oAPIClient:
|
|||
return self.token['token']
|
||||
|
||||
def _request(self, method, url, headers={}, params={}, *args, **kwargs):
|
||||
self.log.info("Building the request {} {}.".format(method.upper(), url))
|
||||
|
||||
# Update headers to force the 'Authorization' field with the right token
|
||||
self.log.debug("Forcing authentication token.")
|
||||
headers.update({
|
||||
'Authorization': 'Token {}'.format(self.get_token())
|
||||
})
|
||||
|
||||
# Use a json format unless the user already specified something
|
||||
if not 'format' in params.keys():
|
||||
self.log.debug("Forcing JSON format response.")
|
||||
params.update({'format': 'json'})
|
||||
|
||||
# Perform the request
|
||||
self.log.info("Performing request {} {}".format(method.upper(), url))
|
||||
response = getattr(requests, method)(
|
||||
url, headers=headers, params=params, *args, **kwargs
|
||||
)
|
||||
self.log.debug("Response code: "+str(response.status_code))
|
||||
|
||||
if response.status_code == requests.codes.unauthorized:
|
||||
# Force re-login to the server (case of a wrong token but valid
|
||||
# credentials) and then retry the request without catching errors.
|
||||
self.log.warning("Token refused. Trying to refresh the token.")
|
||||
self._force_renew_token()
|
||||
|
||||
headers.update({
|
||||
'Authorization': 'Token {}'.format(self.get_token())
|
||||
})
|
||||
self.log.info("Re-performing the request {} {}"
|
||||
.format(method.upper(), url))
|
||||
response = getattr(requests, method)(
|
||||
url, headers=headers, params=params, *args, **kwargs
|
||||
)
|
||||
self.log.debug("Response code: "+str(response.status_code))
|
||||
|
||||
if response.status_code == requests.codes.forbidden:
|
||||
raise exceptions.PermissionDenied(method, url, self._username)
|
||||
e = exceptions.PermissionDenied(method, url, self._username)
|
||||
self.log.debug(e)
|
||||
raise e
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
ret = response.json()
|
||||
self.log.debug("Request {} {} successful.".format(method, url))
|
||||
return ret
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Performs a DELETE request.
|
||||
|
@ -370,7 +441,7 @@ class Re2oAPIClient:
|
|||
return '{proto}://{host}{endpoint}'.format(
|
||||
proto=('https' if self.use_tls else 'http'),
|
||||
host=self.hostname,
|
||||
endpoint=endpoints.get_endpoint_for(name, **kwargs)
|
||||
endpoint=endpoints.get_endpoint_for(name, self.log, **kwargs)
|
||||
)
|
||||
|
||||
def _list_for(self, obj_name, max_results=None, params={}, **kwargs):
|
||||
|
@ -391,9 +462,13 @@ class Re2oAPIClient:
|
|||
exceptions.PermissionDenied: The user does not have the right
|
||||
to perform this request.
|
||||
"""
|
||||
self.log.info("Starting listing {} objects".format(obj_name))
|
||||
self.log.debug("max_results = "+str(max_results))
|
||||
|
||||
# For optimization, list all results in one page unless the user
|
||||
# is forcing the use of a different `page_size`.
|
||||
if not 'page_size' in params.keys():
|
||||
self.log.debug("Forcing 'page_size' parameter to 'all'.")
|
||||
params['page_size'] = 'all'
|
||||
|
||||
# Performs the request for the first page
|
||||
|
@ -410,7 +485,9 @@ class Re2oAPIClient:
|
|||
results += response['results']
|
||||
|
||||
# Returns the exact number of results if applicable
|
||||
return results[:max_results] if max_results else results
|
||||
ret = results[:max_results] if max_results else results
|
||||
self.log.debug("Listing {} objects successful".format(obj_name))
|
||||
return ret
|
||||
|
||||
def _count_for(self, obj_name, params={}, **kwargs):
|
||||
"""Count all '{obj_name}' objects on the server:
|
||||
|
@ -430,18 +507,24 @@ class Re2oAPIClient:
|
|||
exceptions.PermissionDenied: The user does not have the right
|
||||
to perform this request.
|
||||
"""
|
||||
self.log.info("Starting counting {} objects".format(obj_name))
|
||||
|
||||
# For optimization, ask fo only 1 result (so the server will take
|
||||
# less time to process the request) unless the user is forcing the
|
||||
# use of a different `page_size`.
|
||||
if not 'page_size' in params.keys():
|
||||
self.log.debug("Forcing 'page_size' parameter to '1'.")
|
||||
params['page_size'] = 1
|
||||
|
||||
# Performs the request and return the `count` value in the response.
|
||||
return self.get(
|
||||
ret = self.get(
|
||||
self.get_url_for('%s-list' % obj_name, **kwargs),
|
||||
params=params
|
||||
)['count']
|
||||
|
||||
self.log.debug("Counting {} objects successful".format(obj_name))
|
||||
return ret
|
||||
|
||||
def _view_for(self, obj_name, params={}, **kwargs):
|
||||
"""Retrieved the details of a '{obj_name}' object from the server.
|
||||
|
||||
|
@ -460,11 +543,15 @@ class Re2oAPIClient:
|
|||
exceptions.PermissionDenied: The user does not have the right
|
||||
to perform this request.
|
||||
"""
|
||||
return self.get(
|
||||
self.log.info("Starting viewing a {} object".format(obj_name))
|
||||
ret = self.get(
|
||||
self.get_url_for('%s-detail' % obj_name, **kwargs),
|
||||
params=params
|
||||
)
|
||||
|
||||
self.log.debug("Viewing {} object successful".format(obj_name))
|
||||
return ret
|
||||
|
||||
def __getattr__(self, item):
|
||||
|
||||
if item.startswith('list_'):
|
||||
|
|
|
@ -113,13 +113,19 @@ def get_names():
|
|||
return urls.keys()
|
||||
|
||||
|
||||
def get_endpoint_for(name, **kwargs):
|
||||
def get_endpoint_for(name, logger=None, **kwargs):
|
||||
try:
|
||||
url=urls[name]
|
||||
except KeyError as e:
|
||||
raise exceptions.URLNameDoesNotExists(name)
|
||||
except KeyError:
|
||||
e = exceptions.URLNameDoesNotExists(name)
|
||||
if logger is not None:
|
||||
logger.warning(e)
|
||||
raise e
|
||||
else:
|
||||
try:
|
||||
return url.format_map(kwargs)
|
||||
except KeyError as e:
|
||||
raise exceptions.URLParameterMissing(e)
|
||||
e = exceptions.URLParameterMissing(name, e)
|
||||
if logger is not None:
|
||||
logger.warning(e)
|
||||
raise e
|
||||
|
|
|
@ -12,7 +12,7 @@ class URLNameDoesNotExists(APIClientGenericError):
|
|||
|
||||
|
||||
class URLParameterMissing(APIClientGenericError):
|
||||
template = "The url named '{}' require the parameter '{}' to be built."
|
||||
template = "The url named '{}' requires the parameter {} to be built."
|
||||
|
||||
|
||||
class InvalidCredentials(APIClientGenericError):
|
||||
|
@ -22,9 +22,14 @@ class InvalidCredentials(APIClientGenericError):
|
|||
class PermissionDenied(APIClientGenericError):
|
||||
template = "The {} request to '{}' was denied for {}."
|
||||
|
||||
|
||||
class TokenFileNotFound(APIClientGenericError):
|
||||
template = "Token file at {} not found."
|
||||
|
||||
|
||||
class TokenFileNotReadable(APIClientGenericError):
|
||||
template = "Token file at {} is not a JSON readable file."
|
||||
|
||||
|
||||
class TokenNotInTokenFile(APIClientGenericError):
|
||||
template = "Token for {}@{} not found in token file ({})."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue