Commit initial
This commit is contained in:
commit
ac3e5414fe
11 changed files with 685 additions and 0 deletions
9
README.md
Normal file
9
README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## Re2o - DHCP
|
||||||
|
|
||||||
|
This service uses Re2o API to generate DHCP leases files
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* python3
|
||||||
|
* requirements in https://gitlab.federez.net/re2o/re2oapi
|
5
config.ini
Normal file
5
config.ini
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[Re2o]
|
||||||
|
hostname = grizzly2o.crans.org
|
||||||
|
username = passoir
|
||||||
|
password = plopplop
|
||||||
|
|
4
config.ini.example
Normal file
4
config.ini.example
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[Re2o]
|
||||||
|
hostname = re2o.example.net
|
||||||
|
username = my_api_username
|
||||||
|
password = my_api_password
|
42
main.py
Normal file
42
main.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from configparser import ConfigParser
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from re2oapi import Re2oAPIClient
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.template import loader, Context
|
||||||
|
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
config = ConfigParser()
|
||||||
|
config.read('config.ini')
|
||||||
|
|
||||||
|
api_hostname = config.get('Re2o', 'hostname')
|
||||||
|
api_password = config.get('Re2o', 'password')
|
||||||
|
api_username = config.get('Re2o', 'username')
|
||||||
|
|
||||||
|
api_client = Re2oAPIClient(api_hostname,api_username,api_password)
|
||||||
|
|
||||||
|
client_hostname = socket.gethostname().split('.',1)[0]
|
||||||
|
|
||||||
|
def notif_end_adhesion(api_client):
|
||||||
|
asso_options = api_client.list("preferences/assooption")
|
||||||
|
from_mail = api_client.list("preferences/generaloption")["email_from"]
|
||||||
|
template = loader.get_template('email_fin_adhesion')
|
||||||
|
|
||||||
|
for result in api_client.list("reminder/get-users"):
|
||||||
|
for user in result["users_to_remind"]:
|
||||||
|
context = Context({
|
||||||
|
'nom': user["get_full_name"],
|
||||||
|
'temps': result["days"],
|
||||||
|
'asso_name': asso_options["name"],
|
||||||
|
'link': asso_options["site_url"]
|
||||||
|
})
|
||||||
|
print('mail envoyé à {}, reminder {} days'.format(user["get_full_name"],result["days"]))
|
||||||
|
send_mail("Avis de fin d'adhésion / End of subscription notice",
|
||||||
|
'',
|
||||||
|
from_mail,
|
||||||
|
user["email"],
|
||||||
|
html_message = template.render(context)
|
||||||
|
)
|
||||||
|
|
1
re2oapi/.gitignore
vendored
Normal file
1
re2oapi/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
**/__pycache__/
|
8
re2oapi/README.md
Normal file
8
re2oapi/README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
## Re2o - API client
|
||||||
|
|
||||||
|
This project is providing an abstraction layer to easily use the API of Re2o.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* python3
|
||||||
|
* python3-iso8601
|
1
re2oapi/__init__.py
Normal file
1
re2oapi/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .re2oapi import *
|
4
re2oapi/re2oapi/__init__.py
Normal file
4
re2oapi/re2oapi/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from .client import Re2oAPIClient
|
||||||
|
from . import exceptions
|
||||||
|
|
||||||
|
__all__ = ['Re2oAPIClient', 'exceptions']
|
557
re2oapi/re2oapi/client.py
Normal file
557
re2oapi/re2oapi/client.py
Normal file
|
@ -0,0 +1,557 @@
|
||||||
|
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
|
27
re2oapi/re2oapi/exceptions.py
Normal file
27
re2oapi/re2oapi/exceptions.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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 ({})."
|
27
templates/email_fin_adhesion
Normal file
27
templates/email_fin_adhesion
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<p>Bonjour {{nom}},</p>
|
||||||
|
|
||||||
|
<P>Ton adhésion arrive à son therme dans {{temps}} jours.</P>
|
||||||
|
|
||||||
|
{% if online %}
|
||||||
|
<p>Tu peux renouveller ton adhésion en ligne sur <a href"{{link}}">Mon Profil</a>.</p>
|
||||||
|
<p>Tu peux également contacter un membre de {{asso_name}} pour renouveller ton adhésion</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Pour renouveller ton adhésion, contacte un membre de {{asso_name}}.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>A bientôt.</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>Hi {{nom}},</p>
|
||||||
|
|
||||||
|
<P>Your subscription comes to an end in {{temps}} days.</P>
|
||||||
|
|
||||||
|
{% if online %}
|
||||||
|
<p>You can renew it online on <a href"{{link}}">My Profil</a>.</p>
|
||||||
|
<p>You can also contat a member of {{asso_name}} to renew it.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>To renew it, contact a member of {{asso_name}}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>Regards.</p>
|
Loading…
Add table
Add a link
Reference in a new issue