Add comments
This commit is contained in:
parent
d37d47430e
commit
baa6de4f27
1 changed files with 299 additions and 82 deletions
|
@ -1,10 +1,10 @@
|
|||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
import datetime
|
||||
import iso8601
|
||||
from pathlib import Path
|
||||
import json
|
||||
import stat
|
||||
import json
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from . import endpoints
|
||||
from . import exceptions
|
||||
|
@ -16,31 +16,44 @@ DEFAULT_TOKEN_FILENAME = '.re2o.token'
|
|||
|
||||
|
||||
class Re2oAPIClient:
|
||||
"""
|
||||
Object to handle the requests to Re2o API in a seemless way.
|
||||
"""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
|
||||
process for you.
|
||||
"""
|
||||
|
||||
def __init__(self, hostname, username, password, token_file=None, use_tls=True):
|
||||
"""
|
||||
Create an API client.
|
||||
"""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`.
|
||||
|
||||
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)
|
||||
|
||||
:param hostname: (str) The hostname of the Re2o server to use
|
||||
:param username: (str) The username to use
|
||||
:param password: (str) The password to use
|
||||
:param token_file: (str) A path to a file where re2o tokens are stored
|
||||
If `None`, then `$HOME/DEFAULT_TOKEN_FILENAME` is used.
|
||||
(Default: None)
|
||||
:param use_tls: (bool) Should the client used TLS (recommended for
|
||||
production). (Default: True)
|
||||
"""
|
||||
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:
|
||||
|
@ -49,22 +62,28 @@ class Re2oAPIClient:
|
|||
|
||||
@property
|
||||
def need_renew_token(self):
|
||||
"""
|
||||
True is the token expiration time is within less than `TIME_FOR_RENEW`
|
||||
seconds
|
||||
"""
|
||||
"""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):
|
||||
# Check the token file exists
|
||||
if not self.token_file.is_file():
|
||||
raise exceptions.TokenFileNotFound(self.token_file)
|
||||
|
||||
# Read the data in the file
|
||||
with self.token_file.open() as f:
|
||||
data = json.load(f)
|
||||
|
||||
try:
|
||||
# Retrieve data for this hostname and this username in the file
|
||||
token_data = data[self.hostname][self._username]
|
||||
return {
|
||||
'token': token_data['token'],
|
||||
|
@ -78,12 +97,16 @@ class Re2oAPIClient:
|
|||
)
|
||||
|
||||
def _save_token_to_file(self):
|
||||
|
||||
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
|
||||
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] = {
|
||||
|
@ -91,6 +114,8 @@ class Re2oAPIClient:
|
|||
'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)
|
||||
|
@ -99,6 +124,8 @@ class Re2oAPIClient:
|
|||
pass
|
||||
|
||||
def _get_token_from_server(self):
|
||||
|
||||
# Perform the authentication request
|
||||
response = requests.post(
|
||||
self.get_url_for('token'),
|
||||
data={'username': self._username, 'password': self._password}
|
||||
|
@ -106,6 +133,8 @@ class Re2oAPIClient:
|
|||
if response.status_code == requests.codes.bad_request:
|
||||
raise exceptions.InvalidCredentials(self._username, self.hostname)
|
||||
response.raise_for_status()
|
||||
|
||||
# Return the token and expiration time
|
||||
response = response.json()
|
||||
return {
|
||||
'token': response['token'],
|
||||
|
@ -113,8 +142,15 @@ class Re2oAPIClient:
|
|||
}
|
||||
|
||||
def get_token(self):
|
||||
"""
|
||||
Returns the token for the current connection
|
||||
"""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:
|
||||
self.token = self._get_token_from_server()
|
||||
|
@ -122,85 +158,199 @@ class Re2oAPIClient:
|
|||
return self.token['token']
|
||||
|
||||
def _request(self, method, url, headers={}, params={}, *args, **kwargs):
|
||||
|
||||
# Update headers to force the 'Authorization' field with the right 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():
|
||||
params.update({'format': 'json'})
|
||||
|
||||
# Perform the request
|
||||
response = getattr(requests, method)(
|
||||
url, headers=headers, params=params, *args, **kwargs
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""
|
||||
DELETE requests on a given URL that acts like `requests.delete` except
|
||||
that authentication to the API is automatically done and JSON response
|
||||
is decoded
|
||||
"""Performs a DELETE request.
|
||||
|
||||
:param url: (str) URL of the requests
|
||||
:param: See `requests.post` params
|
||||
:returns: (dict) The JSON-decoded result of the 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):
|
||||
"""
|
||||
GET requests on a given URL that acts like `requests.get` except that
|
||||
authentication to the API is automatically done and JSON response is
|
||||
decoded
|
||||
"""Performs a GET request.
|
||||
|
||||
:param url: (str) URL of the requests
|
||||
:param: See `requests.get` params
|
||||
:returns: (dict) The JSON-decoded result of the 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):
|
||||
"""
|
||||
PATCH requests on a given URL that acts like `requests.patch` except
|
||||
that authentication to the API is automatically done and JSON response
|
||||
is decoded
|
||||
"""Performs a PATCH request.
|
||||
|
||||
:param url: (str) URL of the requests
|
||||
:param: See `requests.post` params
|
||||
:returns: (dict) The JSON-decoded result of the 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):
|
||||
"""
|
||||
POST requests on a given URL that acts like `requests.post` except
|
||||
that authentication to the API is automatically done and JSON response
|
||||
is decoded
|
||||
"""Performs a POST request.
|
||||
|
||||
:param url: (str) URL of the requests
|
||||
:param: See `requests.post` params
|
||||
:returns: (dict) The JSON-decoded result of the 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):
|
||||
"""
|
||||
PUT requests on a given URL that acts like `requests.put` except
|
||||
that authentication to the API is automatically done and JSON response
|
||||
is decoded
|
||||
"""Performs a PUT request.
|
||||
|
||||
:param url: (str) URL of the requests
|
||||
:param: See `requests.post` params
|
||||
:returns: (dict) The JSON-decoded result of the 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, name, **kwargs):
|
||||
"""
|
||||
Retrieve the complete URL to use for a given endpoint's name
|
||||
|
||||
:param name: The name of the endpoint to look for.
|
||||
`re2oapi.exception.NameNotExists` is raised of the name does not
|
||||
correspond to any endpoint
|
||||
:param kwargs: A dict with the parameters to use to format the URL
|
||||
def get_url_for(self, name, **kwargs):
|
||||
"""Retrieve the complete URL to use for a given endpoint's name.
|
||||
|
||||
Args:
|
||||
name: The name of the endpoint to look for.
|
||||
**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}{endpoint}'.format(
|
||||
proto=('https' if self.use_tls else 'http'),
|
||||
|
@ -209,28 +359,92 @@ class Re2oAPIClient:
|
|||
)
|
||||
|
||||
def _list_for(self, obj_name, max_results=None, params={}, **kwargs):
|
||||
"""List all '{obj_name}' objects on the server.
|
||||
|
||||
Args:
|
||||
params: See `requests.get` params.
|
||||
**kwargs: A dictionnary used to defines the required parameters
|
||||
in order to build the URL (using `.format()`).
|
||||
|
||||
Returns:
|
||||
The list of all the {obj_name} 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.
|
||||
"""
|
||||
# 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():
|
||||
params['page_size'] = 'all'
|
||||
|
||||
# Performs the request for the first page
|
||||
response = self.get(
|
||||
self.get_url_for('%s-list' % obj_name, **kwargs),
|
||||
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
|
||||
return results[:max_results] if max_results else results
|
||||
|
||||
def _count_for(self, obj_name, params={}, **kwargs):
|
||||
"""Count all '{obj_name}' objects on the server:
|
||||
|
||||
Args:
|
||||
params: See `requests.get` params.
|
||||
**kwargs: A dictionnary used to defines the required parameters
|
||||
in order to build the URL (using `.format()`).
|
||||
|
||||
Returns:
|
||||
The number of {obj_name} 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.
|
||||
"""
|
||||
# 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():
|
||||
params['page_size'] = 1
|
||||
|
||||
# Performs the request and return the `count` value in the response.
|
||||
return self.get(
|
||||
self.get_url_for('%s-list' % obj_name, **kwargs),
|
||||
params=params
|
||||
)['count']
|
||||
|
||||
def _view_for(self, obj_name, params={}, **kwargs):
|
||||
"""Retrieved the details of a '{obj_name}' object from the server.
|
||||
|
||||
Args:
|
||||
params: See `requests.get` params.
|
||||
**kwargs: A dictionnary used to defines the required parameters
|
||||
in order to build the URL (using `.format()`).
|
||||
|
||||
Returns:
|
||||
The serialized data of the queried {obj_name} 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.
|
||||
"""
|
||||
return self.get(
|
||||
self.get_url_for('%s-detail' % obj_name, **kwargs),
|
||||
params=params
|
||||
|
@ -239,26 +453,29 @@ class Re2oAPIClient:
|
|||
def __getattr__(self, item):
|
||||
|
||||
if item.startswith('list_'):
|
||||
return lambda *args, **kwargs: self._list_for(
|
||||
item[len('list_'):],
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
obj_name = item[len('list_'):]
|
||||
def f(*args, **kwargs):
|
||||
return self._list_for(obj_name, *args, **kwargs)
|
||||
f.__doc__ = self._list_for.__doc__.format(obj_name=obj_name)
|
||||
f.__name__ = "list_"+obj_name
|
||||
return f
|
||||
|
||||
elif item.startswith('count_'):
|
||||
return lambda *args, **kwargs: self._count_for(
|
||||
item[len('count_'):],
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
obj_name = item[len('count_'):]
|
||||
def f(*args, **kwargs):
|
||||
return self._count_for(obj_name, *args, **kwargs)
|
||||
f.__doc__ = self._count_for.__doc__.format(obj_name=obj_name)
|
||||
f.__name__ = "count_"+obj_name
|
||||
return f
|
||||
|
||||
elif item.startswith('view_'):
|
||||
return lambda *args, **kwargs: self._view_for(
|
||||
item[len('view_'):],
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
obj_name = item[len('view_'):]
|
||||
def f(*args, **kwargs):
|
||||
return self._view_for(obj_name, *args, **kwargs)
|
||||
f.__doc__ = self._view_for.__doc__.format(obj_name=obj_name)
|
||||
f.__name__ = "view_"+obj_name
|
||||
return f
|
||||
|
||||
else:
|
||||
raise AttributeError(item)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue