Add logging everywhere

This commit is contained in:
Maël Kervella 2018-06-25 16:31:50 +00:00
parent 5b4523c797
commit 2deb99c51c
5 changed files with 124 additions and 25 deletions

View file

@ -1,4 +1 @@
from .re2oapi.client import Re2oAPIClient from .re2oapi import *
from .re2oapi import exceptions
__all__ = ['Re2oAPIClient', 'exceptions']

View file

@ -0,0 +1,4 @@
from .client import Re2oAPIClient
from . import exceptions
__all__ = ['Re2oAPIClient', 'exceptions']

View file

@ -1,3 +1,4 @@
import logging
import datetime import datetime
import iso8601 import iso8601
from pathlib import Path from pathlib import Path
@ -23,7 +24,8 @@ class Re2oAPIClient:
process for you. 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. """Creates an API client.
Args: Args:
@ -36,7 +38,9 @@ class Re2oAPIClient:
default value is `None`, which indicated to use default value is `None`, which indicated to use
`$HOME/{DEFAULT_TOKEN_FILENAME}`. `$HOME/{DEFAULT_TOKEN_FILENAME}`.
use_tls: A boolean to indicate whether the client should us TLS 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: Raises:
requests.exceptions.ConnectionError: Unable to resolve the requests.exceptions.ConnectionError: Unable to resolve the
@ -47,6 +51,24 @@ class Re2oAPIClient:
are not valid according to the Re2o server. are not valid according to the Re2o server.
""".format(DEFAULT_TOKEN_FILENAME=DEFAULT_TOKEN_FILENAME) """.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.use_tls = use_tls
self.token_file = token_file or Path.home() / DEFAULT_TOKEN_FILENAME self.token_file = token_file or Path.home() / DEFAULT_TOKEN_FILENAME
self.hostname = hostname self.hostname = hostname
@ -73,29 +95,47 @@ class Re2oAPIClient:
datetime.timedelta(seconds=TIME_FOR_RENEW) datetime.timedelta(seconds=TIME_FOR_RENEW)
def _get_token_from_file(self): def _get_token_from_file(self):
self.log.debug("Retrieving token from token file '{}'."
.format(self.token_file))
# Check the token file exists # Check the token file exists
if not self.token_file.is_file(): 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 # Read the data in the file
try:
with self.token_file.open() as f: with self.token_file.open() as f:
data = json.load(f) data = json.load(f)
except Exception:
e = exceptions.TokenFileNotReadable(self.token_file)
self.log.error(e)
raise e
try: try:
# Retrieve data for this hostname and this username in the file # Retrieve data for this hostname and this username in the file
token_data = data[self.hostname][self._username] token_data = data[self.hostname][self._username]
return { ret = {
'token': token_data['token'], 'token': token_data['token'],
'expiration': iso8601.parse_date(token_data['expiration']) 'expiration': iso8601.parse_date(token_data['expiration'])
} }
except KeyError: except KeyError:
raise exceptions.TokenNotInTokenFile( e = exceptions.TokenNotInTokenFile(
self._username, self._username,
self.hostname, self.hostname,
self.token_file 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): def _save_token_to_file(self):
self.log.debug("Saving token to token file '{}'."
.format(self.token_file))
try: try:
# Read previous data to not erase other tokens # Read previous data to not erase other tokens
@ -103,6 +143,8 @@ class Re2oAPIClient:
data = json.load(f) data = json.load(f)
except Exception: except Exception:
# Default value if file not readbale or data not valid JSON # 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 = {} data = {}
# Insert the new token in the data (replace old token if it exists) # Insert the new token in the data (replace old token if it exists)
@ -120,25 +162,38 @@ class Re2oAPIClient:
json.dump(data, f) json.dump(data, f)
self.token_file.chmod(stat.S_IWRITE | stat.S_IREAD) self.token_file.chmod(stat.S_IWRITE | stat.S_IREAD)
except Exception: 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): 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 # Perform the authentication request
response = requests.post( response = requests.post(
self.get_url_for('token'), self.get_url_for('token'),
data={'username': self._username, 'password': self._password} data={'username': self._username, 'password': self._password}
) )
self.log.debug("Response code: "+str(response.status_code))
if response.status_code == requests.codes.bad_request: 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() response.raise_for_status()
# Return the token and expiration time # Return the token and expiration time
response = response.json() response = response.json()
return { ret = {
'token': response['token'], 'token': response['token'],
'expiration': iso8601.parse_date(response['expiration']) 'expiration': iso8601.parse_date(response['expiration'])
} }
self.log.debug("Token successfully retrieved from server '{}'."
.format(self.hostname))
return ret
def _force_renew_token(self): def _force_renew_token(self):
self.token = self._get_token_from_server() self.token = self._get_token_from_server()
@ -161,35 +216,51 @@ class Re2oAPIClient:
return self.token['token'] return self.token['token']
def _request(self, method, url, headers={}, params={}, *args, **kwargs): 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 # Update headers to force the 'Authorization' field with the right token
self.log.debug("Forcing authentication token.")
headers.update({ headers.update({
'Authorization': 'Token {}'.format(self.get_token()) 'Authorization': 'Token {}'.format(self.get_token())
}) })
# Use a json format unless the user already specified something # Use a json format unless the user already specified something
if not 'format' in params.keys(): if not 'format' in params.keys():
self.log.debug("Forcing JSON format response.")
params.update({'format': 'json'}) params.update({'format': 'json'})
# Perform the request # Perform the request
self.log.info("Performing request {} {}".format(method.upper(), url))
response = getattr(requests, method)( response = getattr(requests, method)(
url, headers=headers, params=params, *args, **kwargs url, headers=headers, params=params, *args, **kwargs
) )
self.log.debug("Response code: "+str(response.status_code))
if response.status_code == requests.codes.unauthorized: if response.status_code == requests.codes.unauthorized:
# Force re-login to the server (case of a wrong token but valid # Force re-login to the server (case of a wrong token but valid
# credentials) and then retry the request without catching errors. # credentials) and then retry the request without catching errors.
self.log.warning("Token refused. Trying to refresh the token.")
self._force_renew_token() self._force_renew_token()
headers.update({ headers.update({
'Authorization': 'Token {}'.format(self.get_token()) 'Authorization': 'Token {}'.format(self.get_token())
}) })
self.log.info("Re-performing the request {} {}"
.format(method.upper(), url))
response = getattr(requests, method)( response = getattr(requests, method)(
url, headers=headers, params=params, *args, **kwargs url, headers=headers, params=params, *args, **kwargs
) )
self.log.debug("Response code: "+str(response.status_code))
if response.status_code == requests.codes.forbidden: 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() 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): def delete(self, *args, **kwargs):
"""Performs a DELETE request. """Performs a DELETE request.
@ -370,7 +441,7 @@ class Re2oAPIClient:
return '{proto}://{host}{endpoint}'.format( return '{proto}://{host}{endpoint}'.format(
proto=('https' if self.use_tls else 'http'), proto=('https' if self.use_tls else 'http'),
host=self.hostname, 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): 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 exceptions.PermissionDenied: The user does not have the right
to perform this request. 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 # For optimization, list all results in one page unless the user
# is forcing the use of a different `page_size`. # is forcing the use of a different `page_size`.
if not 'page_size' in params.keys(): if not 'page_size' in params.keys():
self.log.debug("Forcing 'page_size' parameter to 'all'.")
params['page_size'] = 'all' params['page_size'] = 'all'
# Performs the request for the first page # Performs the request for the first page
@ -410,7 +485,9 @@ class Re2oAPIClient:
results += response['results'] results += response['results']
# Returns the exact number of results if applicable # 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): def _count_for(self, obj_name, params={}, **kwargs):
"""Count all '{obj_name}' objects on the server: """Count all '{obj_name}' objects on the server:
@ -430,18 +507,24 @@ class Re2oAPIClient:
exceptions.PermissionDenied: The user does not have the right exceptions.PermissionDenied: The user does not have the right
to perform this request. 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 # For optimization, ask fo only 1 result (so the server will take
# less time to process the request) unless the user is forcing the # less time to process the request) unless the user is forcing the
# use of a different `page_size`. # use of a different `page_size`.
if not 'page_size' in params.keys(): if not 'page_size' in params.keys():
self.log.debug("Forcing 'page_size' parameter to '1'.")
params['page_size'] = 1 params['page_size'] = 1
# Performs the request and return the `count` value in the response. # 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), self.get_url_for('%s-list' % obj_name, **kwargs),
params=params params=params
)['count'] )['count']
self.log.debug("Counting {} objects successful".format(obj_name))
return ret
def _view_for(self, obj_name, params={}, **kwargs): def _view_for(self, obj_name, params={}, **kwargs):
"""Retrieved the details of a '{obj_name}' object from the server. """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 exceptions.PermissionDenied: The user does not have the right
to perform this request. 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), self.get_url_for('%s-detail' % obj_name, **kwargs),
params=params params=params
) )
self.log.debug("Viewing {} object successful".format(obj_name))
return ret
def __getattr__(self, item): def __getattr__(self, item):
if item.startswith('list_'): if item.startswith('list_'):

View file

@ -113,13 +113,19 @@ def get_names():
return urls.keys() return urls.keys()
def get_endpoint_for(name, **kwargs): def get_endpoint_for(name, logger=None, **kwargs):
try: try:
url=urls[name] url=urls[name]
except KeyError as e: except KeyError:
raise exceptions.URLNameDoesNotExists(name) e = exceptions.URLNameDoesNotExists(name)
if logger is not None:
logger.warning(e)
raise e
else: else:
try: try:
return url.format_map(kwargs) return url.format_map(kwargs)
except KeyError as e: except KeyError as e:
raise exceptions.URLParameterMissing(e) e = exceptions.URLParameterMissing(name, e)
if logger is not None:
logger.warning(e)
raise e

View file

@ -12,7 +12,7 @@ class URLNameDoesNotExists(APIClientGenericError):
class URLParameterMissing(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): class InvalidCredentials(APIClientGenericError):
@ -22,9 +22,14 @@ class InvalidCredentials(APIClientGenericError):
class PermissionDenied(APIClientGenericError): class PermissionDenied(APIClientGenericError):
template = "The {} request to '{}' was denied for {}." template = "The {} request to '{}' was denied for {}."
class TokenFileNotFound(APIClientGenericError): class TokenFileNotFound(APIClientGenericError):
template = "Token file at {} not found." template = "Token file at {} not found."
class TokenFileNotReadable(APIClientGenericError):
template = "Token file at {} is not a JSON readable file."
class TokenNotInTokenFile(APIClientGenericError): class TokenNotInTokenFile(APIClientGenericError):
template = "Token for {}@{} not found in token file ({})." template = "Token for {}@{} not found in token file ({})."