Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
nanoy
729cfcb7ac Merge branch 'better_403_error' into 'master'
Better 403 error message

See merge request re2o/re2oapi!2
2021-01-12 22:28:22 +01:00
6d930e37ec Better 403 error message 2021-01-12 21:37:59 +01:00
detraz
ffaed92103 Merge branch 'master' of https://gitlab.federez.net/re2o/re2oapi 2019-10-06 16:03:33 +02:00
Hugo Levy-Falk
1f8366055d Better handle of redirections. 2019-09-28 13:32:04 +02:00
Hugo Levy-Falk
b12df74fe7 Fix indentation 2019-03-12 22:05:32 +01:00
Gabriel Detraz
0dd459e3ec unindent error 2018-11-14 17:14:27 +01:00
Gabriel Detraz
b4906d8b25 Api for sending mail 2018-11-14 17:08:55 +01:00
3 changed files with 99 additions and 66 deletions

View file

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

View file

@ -5,9 +5,11 @@ from pathlib import Path
import stat import stat
import json import json
import requests import requests
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from . import endpoints
from . import exceptions from . import exceptions
# Number of seconds before expiration where renewing the token is done # Number of seconds before expiration where renewing the token is done
@ -33,23 +35,23 @@ class Re2oAPIClient:
username: The username to use. username: The username to use.
password: The password to use. password: The password to use.
token_file: An optional path to the file where re2o tokens are token_file: An optional path to the file where re2o tokens are
stored. Used both for retrieving the token and saving it, so stored. Used both for retrieving the token and saving it, so
the file must be accessible for reading and writing. The the file must be accessible for reading and writing. The
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 log_level: Control the logging level to use. The default is
`logging.CRITICAL+10`. So nothing is logged. `logging.CRITICAL+10`. So nothing is logged.
Raises: Raises:
requests.exceptions.ConnectionError: Unable to resolve the requests.exceptions.ConnectionError: Unable to resolve the
provided hostname. provided hostname.
requests.exceptions.HTTPError: The server used does not have a requests.exceptions.HTTPError: The server used does not have a
valid Re2o API. valid Re2o API.
re2oapi.exceptions.InvalidCredentials: The credentials provided re2oapi.exceptions.InvalidCredentials: The credentials provided
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 # Enable logging
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
@ -61,7 +63,7 @@ class Re2oAPIClient:
) )
handler.setFormatter(formatter) handler.setFormatter(formatter)
self.log.addHandler(handler) self.log.addHandler(handler)
self.log.setLevel(log_level) self.log.setLevel(log_level)
self.log.info("Starting new Re2o API client.") self.log.info("Starting new Re2o API client.")
self.log.debug("hostname = " + str(hostname)) self.log.debug("hostname = " + str(hostname))
@ -87,8 +89,8 @@ class Re2oAPIClient:
Returns: Returns:
True is the token expiration time is within less than True is the token expiration time is within less than
{TIME_FOR_RENEW} seconds. {TIME_FOR_RENEW} seconds.
""".format(TIME_FOR_RENEW=TIME_FOR_RENEW) """.format(TIME_FOR_RENEW=TIME_FOR_RENEW)
return self.token['expiration'] < \ return self.token['expiration'] < \
datetime.datetime.now(datetime.timezone.utc) + \ datetime.datetime.now(datetime.timezone.utc) + \
@ -131,7 +133,7 @@ class Re2oAPIClient:
else: else:
self.log.debug("Token successfully retrieved from token " self.log.debug("Token successfully retrieved from token "
"file '{}'.".format(self.token_file)) "file '{}'.".format(self.token_file))
return ret return ret
def _save_token_to_file(self): def _save_token_to_file(self):
self.log.debug("Saving token to token file '{}'." self.log.debug("Saving token to token file '{}'."
@ -150,20 +152,20 @@ class Re2oAPIClient:
# 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)
if self.hostname not in data.keys(): if self.hostname not in data.keys():
data[self.hostname] = {} data[self.hostname] = {}
data[self.hostname][self._username] = { data[self.hostname][self._username] = {
'token': self.token['token'], 'token': self.token['token'],
'expiration': self.token['expiration'].isoformat() 'expiration': self.token['expiration'].isoformat()
} }
# Rewrite the token file and ensure only the user can read it # Rewrite the token file and ensure only the user can read it
# Fails silently if the file cannot be written. # Fails silently if the file cannot be written.
try: try:
with self.token_file.open('w') as f: with self.token_file.open('w') as f:
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:
self.log.error("Token file '{}' could not be written. Passing." self.log.error("Token file '{}' could not be written. Passing."
.format(self.token_file)) .format(self.token_file))
else: else:
self.log.debug("Token sucessfully writen in token file '{}'." self.log.debug("Token sucessfully writen in token file '{}'."
.format(self.token_file)) .format(self.token_file))
@ -208,8 +210,8 @@ class Re2oAPIClient:
Raises: Raises:
re2oapi.exceptions.InvalidCredentials: The token needs to be re2oapi.exceptions.InvalidCredentials: The token needs to be
renewed but the given credentials are not valid. renewed but the given credentials are not valid.
""" """
if self.need_renew_token: if self.need_renew_token:
# Renew the token only if needed # Renew the token only if needed
self._force_renew_token() self._force_renew_token()
@ -220,6 +222,7 @@ class Re2oAPIClient:
# 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.") self.log.debug("Forcing authentication token.")
self.log.debug("Token =" + str(self.get_token()))
headers.update({ headers.update({
'Authorization': 'Token {}'.format(self.get_token()) 'Authorization': 'Token {}'.format(self.get_token())
}) })
@ -232,10 +235,19 @@ class Re2oAPIClient:
# Perform the request # Perform the request
self.log.info("Performing request {} {}".format(method.upper(), url)) 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,
allow_redirects=False, *args, **kwargs
) )
self.log.debug("Response code: "+str(response.status_code)) self.log.debug("Response code: "+str(response.status_code))
if response.is_redirect:
self.log.debug("Redirection detected.")
response = getattr(requests, method)(
response.headers['Location'], headers=headers, params=params,
allow_redirects=False, *args, **kwargs
)
self.log.debug("Response code after redirection: "+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.
@ -246,20 +258,20 @@ class Re2oAPIClient:
'Authorization': 'Token {}'.format(self.get_token()) 'Authorization': 'Token {}'.format(self.get_token())
}) })
self.log.info("Re-performing the request {} {}" self.log.info("Re-performing the request {} {}"
.format(method.upper(), url)) .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)) self.log.debug("Response code: "+str(response.status_code))
if response.status_code == requests.codes.forbidden: if response.status_code == requests.codes.forbidden:
e = exceptions.PermissionDenied(method, url, self._username) e = exceptions.PermissionDenied(method, url, self._username, response.reason)
self.log.debug(e) self.log.debug(e)
raise e raise e
response.raise_for_status() response.raise_for_status()
ret = response.json() ret = response.json()
self.log.debug("Request {} {} successful.".format(method, url)) self.log.debug("Request {} {} successful.".format(method, response.url))
return ret return ret
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
@ -279,10 +291,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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._request('delete', *args, **kwargs) return self._request('delete', *args, **kwargs)
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@ -302,10 +314,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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._request('get', *args, **kwargs) return self._request('get', *args, **kwargs)
def head(self, *args, **kwargs): def head(self, *args, **kwargs):
@ -325,10 +337,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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._request('get', *args, **kwargs) return self._request('get', *args, **kwargs)
def option(self, *args, **kwargs): def option(self, *args, **kwargs):
@ -348,10 +360,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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._request('get', *args, **kwargs) return self._request('get', *args, **kwargs)
def patch(self, *args, **kwargs): def patch(self, *args, **kwargs):
@ -371,10 +383,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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._request('patch', *args, **kwargs) return self._request('patch', *args, **kwargs)
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
@ -394,10 +406,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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._request('post', *args, **kwargs) return self._request('post', *args, **kwargs)
def put(self, *args, **kwargs): def put(self, *args, **kwargs):
@ -417,10 +429,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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._request('put', *args, **kwargs) return self._request('put', *args, **kwargs)
def get_url_for(self, endpoint): def get_url_for(self, endpoint):
@ -429,15 +441,15 @@ class Re2oAPIClient:
Args: Args:
endpoint: The path of the endpoint. endpoint: The path of the endpoint.
**kwargs: A dictionnary with the parameter to use to build the **kwargs: A dictionnary with the parameter to use to build the
URL (using .format() syntax) URL (using .format() syntax)
Returns: Returns:
The full URL to use. The full URL to use.
Raises: Raises:
re2oapi.exception.NameNotExists: The provided name does not re2oapi.exception.NameNotExists: The provided name does not
correspond to any endpoint. correspond to any endpoint.
""" """
return '{proto}://{host}/{namespace}/{endpoint}'.format( return '{proto}://{host}/{namespace}/{endpoint}'.format(
proto=('https' if self.use_tls else 'http'), proto=('https' if self.use_tls else 'http'),
host=self.hostname, host=self.hostname,
@ -459,12 +471,12 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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 under '{}'" self.log.info("Starting listing objects under '{}'"
.format(endpoint)) .format(endpoint))
self.log.debug("max_results = "+str(max_results)) 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
@ -482,7 +494,7 @@ class Re2oAPIClient:
# Get all next pages and append the results # Get all next pages and append the results
while response['next'] is not None and \ while response['next'] is not None and \
(max_results is None or len(results) < max_results): (max_results is None or len(results) < max_results):
response = self.get(response['next']) response = self.get(response['next'])
results += response['results'] results += response['results']
@ -505,12 +517,12 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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 under '{}'" self.log.info("Starting counting objects under '{}'"
.format(endpoint)) .format(endpoint))
# For optimization, ask for only 1 result (so the server will take # For optimization, ask for 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
@ -542,10 +554,10 @@ class Re2oAPIClient:
Raises: Raises:
requests.exceptions.RequestException: An error occured while requests.exceptions.RequestException: An error occured while
performing the request. performing the request.
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 viewing an object under '{}'" self.log.info("Starting viewing an object under '{}'"
.format(endpoint)) .format(endpoint))
ret = self.get( ret = self.get(
@ -556,3 +568,24 @@ class Re2oAPIClient:
self.log.debug("Viewing object under '{}' successful" self.log.debug("Viewing object under '{}' successful"
.format(endpoint)) .format(endpoint))
return ret return ret
class ApiSendMail:
"""Basic api for sending mails"""
def __init__(self, server, port, starttls=False):
"""Give here the server, the port and tls or not"""
self.connection = smtplib.SMTP(server, port)
if starttls:
self.connection.starttls()
def send_mail(self, email_from, email_to, subject, body, mode='html'):
"""Sending mail from from, to, subject and body"""
self.msg = MIMEMultipart()
self.msg['From'] = email_from
self.msg['To'] = email_to
self.msg['Subject'] = subject
self.msg.attach(MIMEText(body, mode))
self.connection.sendmail(email_from, email_to, self.msg.as_string())
def close(self):
self.connection.quit()

View file

@ -12,7 +12,7 @@ class InvalidCredentials(APIClientGenericError):
class PermissionDenied(APIClientGenericError): class PermissionDenied(APIClientGenericError):
template = "The {} request to '{}' was denied for {}." template = "The {} request to '{}' was denied for {} (reason: {})."
class TokenFileNotFound(APIClientGenericError): class TokenFileNotFound(APIClientGenericError):