From 95daef9b9c0ac7823c3ab9287b64ff8c7998821a Mon Sep 17 00:00:00 2001 From: Benjamin Graillot Date: Tue, 7 Aug 2018 10:39:45 +0200 Subject: [PATCH] Ajout de re2oapi --- .gitignore | 1 + .gitmodules | 3 + re2oapi | 1 + re2oapi/.gitignore | 1 - re2oapi/README.md | 8 - re2oapi/__init__.py | 1 - re2oapi/re2oapi/__init__.py | 4 - re2oapi/re2oapi/client.py | 557 ---------------------------------- re2oapi/re2oapi/exceptions.py | 27 -- 9 files changed, 5 insertions(+), 598 deletions(-) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 re2oapi delete mode 100644 re2oapi/.gitignore delete mode 100644 re2oapi/README.md delete mode 100644 re2oapi/__init__.py delete mode 100644 re2oapi/re2oapi/__init__.py delete mode 100644 re2oapi/re2oapi/client.py delete mode 100644 re2oapi/re2oapi/exceptions.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fa7ce7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.ini diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a0d74b4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "re2oapi"] + path = re2oapi + url = https://gitlab.adm.crans.org/nounous/re2oapi.git diff --git a/re2oapi b/re2oapi new file mode 160000 index 0000000..c3277c2 --- /dev/null +++ b/re2oapi @@ -0,0 +1 @@ +Subproject commit c3277c2e6eb3a85e8580c906266cad46c4043677 diff --git a/re2oapi/.gitignore b/re2oapi/.gitignore deleted file mode 100644 index 61f2dc9..0000000 --- a/re2oapi/.gitignore +++ /dev/null @@ -1 +0,0 @@ -**/__pycache__/ diff --git a/re2oapi/README.md b/re2oapi/README.md deleted file mode 100644 index f4196a1..0000000 --- a/re2oapi/README.md +++ /dev/null @@ -1,8 +0,0 @@ -## Re2o - API client - -This project is providing an abstraction layer to easily use the API of Re2o. - -## Requirements - -* python3 -* python3-iso8601 diff --git a/re2oapi/__init__.py b/re2oapi/__init__.py deleted file mode 100644 index 92f9065..0000000 --- a/re2oapi/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .re2oapi import * diff --git a/re2oapi/re2oapi/__init__.py b/re2oapi/re2oapi/__init__.py deleted file mode 100644 index 5939662..0000000 --- a/re2oapi/re2oapi/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .client import Re2oAPIClient -from . import exceptions - -__all__ = ['Re2oAPIClient', 'exceptions'] diff --git a/re2oapi/re2oapi/client.py b/re2oapi/re2oapi/client.py deleted file mode 100644 index 6348c8e..0000000 --- a/re2oapi/re2oapi/client.py +++ /dev/null @@ -1,557 +0,0 @@ -import logging -import datetime -import iso8601 -from pathlib import Path -import stat -import json -import requests -from requests.exceptions import HTTPError - -from . import exceptions - -# Number of seconds before expiration where renewing the token is done -TIME_FOR_RENEW = 60 -# Default name of the file to store tokens -DEFAULT_TOKEN_FILENAME = '.re2o.token' - - -class Re2oAPIClient: - """Wrapper to handle the requests to Re2o API in a seemless way. - - You must first initialize the client and then you can access - the API with dedicated functions that handle the authentication - process for you. - """ - - def __init__(self, hostname, username, password, token_file=None, - use_tls=True, log_level=logging.CRITICAL+10): - """Creates an API client. - - Args: - hostname: The hostname of the Re2o server to use. - username: The username to use. - password: The password to use. - token_file: An optional path to the file where re2o tokens are - stored. Used both for retrieving the token and saving it, so - the file must be accessible for reading and writing. The - 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`. - 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 - provided hostname. - requests.exceptions.HTTPError: The server used does not have a - valid Re2o API. - re2oapi.exceptions.InvalidCredentials: The credentials provided - 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 - self._username = username - self._password = password - # Try to fetch token from token file else get a new one from the - # server - try: - self.token = self._get_token_from_file() - except exceptions.APIClientGenericError: - self._force_renew_token() - - @property - def need_renew_token(self): - """The token needs to be renewed. - - Returns: - True is the token expiration time is within less than - {TIME_FOR_RENEW} seconds. - """.format(TIME_FOR_RENEW=TIME_FOR_RENEW) - - return self.token['expiration'] < \ - datetime.datetime.now(datetime.timezone.utc) + \ - 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(): - 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] - ret = { - 'token': token_data['token'], - 'expiration': iso8601.parse_date(token_data['expiration']) - } - except KeyError: - 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 - with self.token_file.open() as f: - 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) - if self.hostname not in data.keys(): - data[self.hostname] = {} - data[self.hostname][self._username] = { - 'token': self.token['token'], - 'expiration': self.token['expiration'].isoformat() - } - - # Rewrite the token file and ensure only the user can read it - # Fails silently if the file cannot be written. - try: - with self.token_file.open('w') as f: - json.dump(data, f) - self.token_file.chmod(stat.S_IWRITE | stat.S_IREAD) - except Exception: - 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-auth'), - data={'username': self._username, 'password': self._password} - ) - self.log.debug("Response code: "+str(response.status_code)) - - if response.status_code == requests.codes.bad_request: - 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() - 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() - self._save_token_to_file() - - def get_token(self): - """Retrieves the token to use for the current connection. - - Returns: - The token to use in the request as an authentication. It is - automatically renewed if needed. - - Raises: - re2oapi.exceptions.InvalidCredentials: The token needs to be - renewed but the given credentials are not valid. - """ - if self.need_renew_token: - # Renew the token only if needed - self._force_renew_token() - 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: - e = exceptions.PermissionDenied(method, url, self._username) - self.log.debug(e) - raise e - response.raise_for_status() - - ret = response.json() - self.log.debug("Request {} {} successful.".format(method, url)) - return ret - - def delete(self, *args, **kwargs): - """Performs a DELETE request. - - DELETE request on a given URL that acts like `requests.delete` except - that authentication to the API is automatically done and JSON response - is decoded. - - Args: - url: The URL of the requests. - *args: See `requests.delete` parameters. - **kwargs: See `requests.delete` parameters. - - Returns: - The JSON-decoded result of the request. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - return self._request('delete', *args, **kwargs) - - def get(self, *args, **kwargs): - """Performs a GET request. - - GET request on a given URL that acts like `requests.get` except that - authentication to the API is automatically done and JSON response is - decoded. - - Args: - url: The URL of the requests. - *args: See `requests.get` parameters. - **kwargs: See `requests.get` parameters. - - Returns: - The JSON-decoded result of the request. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - return self._request('get', *args, **kwargs) - - def head(self, *args, **kwargs): - """Performs a HEAD request. - - HEAD request on a given URL that acts like `requests.head` except that - authentication to the API is automatically done and JSON response is - decoded. - - Args: - url: The URL of the requests. - *args: See `requests.head` parameters. - **kwargs: See `requests.head` parameters. - - Returns: - The JSON-decoded result of the request. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - return self._request('get', *args, **kwargs) - - def option(self, *args, **kwargs): - """Performs a OPTION request. - - OPTION request on a given URL that acts like `requests.option` except - that authentication to the API is automatically done and JSON response - is decoded. - - Args: - url: The URL of the requests. - *args: See `requests.option` parameters. - **kwargs: See `requests.option` parameters. - - Returns: - The JSON-decoded result of the request. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - return self._request('get', *args, **kwargs) - - def patch(self, *args, **kwargs): - """Performs a PATCH request. - - PATCH request on a given URL that acts like `requests.patch` except - that authentication to the API is automatically done and JSON response - is decoded. - - Args: - url: The URL of the requests. - *args: See `requests.patch` parameters. - **kwargs: See `requests.patch` parameters. - - Returns: - The JSON-decoded result of the request. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - return self._request('patch', *args, **kwargs) - - def post(self, *args, **kwargs): - """Performs a POST request. - - POST request on a given URL that acts like `requests.post` except that - authentication to the API is automatically done and JSON response is - decoded. - - Args: - url: The URL of the requests. - *args: See `requests.post` parameters. - **kwargs: See `requests.post` parameters. - - Returns: - The JSON-decoded result of the request. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - return self._request('post', *args, **kwargs) - - def put(self, *args, **kwargs): - """Performs a PUT request. - - PUT request on a given URL that acts like `requests.put` except that - authentication to the API is automatically done and JSON response is - decoded. - - Args: - url: The URL of the requests. - *args: See `requests.put` parameters. - **kwargs: See `requests.put` parameters. - - Returns: - The JSON-decoded result of the request. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - return self._request('put', *args, **kwargs) - - def get_url_for(self, endpoint): - """Retrieve the complete URL to use for a given endpoint's name. - - Args: - endpoint: The path of the endpoint. - **kwargs: A dictionnary with the parameter to use to build the - URL (using .format() syntax) - - Returns: - The full URL to use. - - Raises: - re2oapi.exception.NameNotExists: The provided name does not - correspond to any endpoint. - """ - return '{proto}://{host}/{namespace}/{endpoint}'.format( - proto=('https' if self.use_tls else 'http'), - host=self.hostname, - namespace='api', - endpoint=endpoint - ) - - def list(self, endpoint, max_results=None, params={}): - """List all objects on the server that corresponds to the given - endpoint. The endpoint must be valid for listing objects. - - Args: - endpoint: The path of the endpoint. - max_results: A limit on the number of result to return - params: See `requests.get` params. - - Returns: - The list of all the objects serialized as returned by the API. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - self.log.info("Starting listing objects under '{}'" - .format(endpoint)) - 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'] = max_results or 'all' - - # Performs the request for the first page - response = self.get( - self.get_url_for(endpoint), - params=params - ) - results = response['results'] - - # Get all next pages and append the results - while response['next'] is not None and \ - (max_results is None or len(results) < max_results): - response = self.get(response['next']) - results += response['results'] - - # Returns the exact number of results if applicable - ret = results[:max_results] if max_results else results - self.log.debug("Listing objects under '{}' successful" - .format(endpoint)) - return ret - - def count(self, endpoint, params={}): - """Count all objects on the server that corresponds to the given - endpoint. The endpoint must be valid for listing objects. - - Args: - endpoint: The path of the endpoint. - params: See `requests.get` params. - - Returns: - The number of objects on the server as returned by the API. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - self.log.info("Starting counting objects under '{}'" - .format(endpoint)) - - # For optimization, ask for 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. - ret = self.get( - self.get_url_for(endpoint), - params=params - )['count'] - - self.log.debug("Counting objects under '{}' successful" - .format(endpoint)) - return ret - - def view(self, endpoint, params={}): - """Retrieved the details of an object from the server that corresponds - to the given endpoint. - - Args: - endpoint: The path of the endpoint. - params: See `requests.get` params. - - Returns: - The serialized data of the queried object as returned by the API. - - Raises: - requests.exceptions.RequestException: An error occured while - performing the request. - exceptions.PermissionDenied: The user does not have the right - to perform this request. - """ - self.log.info("Starting viewing an object under '{}'" - .format(endpoint)) - ret = self.get( - self.get_url_for(endpoint), - params=params - ) - - self.log.debug("Viewing object under '{}' successful" - .format(endpoint)) - return ret diff --git a/re2oapi/re2oapi/exceptions.py b/re2oapi/re2oapi/exceptions.py deleted file mode 100644 index 7522485..0000000 --- a/re2oapi/re2oapi/exceptions.py +++ /dev/null @@ -1,27 +0,0 @@ -class APIClientGenericError(ValueError): - template = "{}" - - def __init__(self, *data): - self.data = data - self.message = self.template.format(*data) - super(APIClientGenericError, self).__init__(self.message) - - -class InvalidCredentials(APIClientGenericError): - template = "The credentials for {}@{} are not valid." - - -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 ({})."