237 lines
8.8 KiB
Python
237 lines
8.8 KiB
Python
# welco - multichannel request processing
|
|
# Copyright (C) 2018 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import base64
|
|
|
|
import requests
|
|
from dateutil.parser import parse as parse_datetime
|
|
from django.utils import six
|
|
from django.utils.six.moves.urllib import parse as urlparse
|
|
from requests.adapters import HTTPAdapter
|
|
from requests.packages.urllib3.util.retry import Retry
|
|
|
|
|
|
class MaarchError(Exception):
|
|
pass
|
|
|
|
|
|
class MaarchCourrier:
|
|
url = None
|
|
username = None
|
|
password = None
|
|
default_limit = 100
|
|
max_retries = 3
|
|
|
|
def __init__(self, url, username, password):
|
|
self.url = url
|
|
self.username = username
|
|
self.password = password
|
|
|
|
def __repr__(self):
|
|
return '<MaarchCourrier url:%s>' % self.url
|
|
|
|
class Courrier:
|
|
content = None
|
|
format = None
|
|
status = None
|
|
|
|
def __init__(self, maarch_courrier, **kwargs):
|
|
self.maarch_courrier = maarch_courrier
|
|
self.pk = kwargs.pop('res_id', None)
|
|
# decode file content
|
|
if 'fileBase64Content' in kwargs:
|
|
kwargs['content'] = base64.b64decode(kwargs.pop('fileBase64Content'))
|
|
# decode date fields
|
|
for key in kwargs:
|
|
if key.endswith('_date') and kwargs[key]:
|
|
kwargs[key] = parse_datetime(kwargs[key])
|
|
self.__dict__.update(kwargs)
|
|
|
|
def __repr__(self):
|
|
descriptions = []
|
|
for key in ['pk', 'status']:
|
|
if getattr(self, key, None):
|
|
descriptions.append('%s:%s' % (key, getattr(self, key)))
|
|
return '<Courrier %s>' % ' '.join(descriptions)
|
|
|
|
@classmethod
|
|
def new_with_file(cls, maarch_courrier, content, format, status, **kwargs):
|
|
if hasattr(content, 'read'):
|
|
content = content.read()
|
|
else:
|
|
content = content
|
|
return cls(maarch_courrier, content=content, format=format, status=status, **kwargs)
|
|
|
|
def post_serialize(self):
|
|
payload = {}
|
|
assert self.content
|
|
assert self.status
|
|
payload['encodedFile'] = base64.b64encode(self.content)
|
|
payload['collId'] = 'letterbox_coll'
|
|
payload['table'] = 'res_letterbox'
|
|
payload['fileFormat'] = self.format
|
|
payload['data'] = d = []
|
|
excluded_keys = ['content', 'format', 'status', 'maarch_courrier', 'pk']
|
|
data = {key: self.__dict__[key] for key in self.__dict__ if key not in excluded_keys}
|
|
if data:
|
|
for key, value in data.items():
|
|
if isinstance(value, str):
|
|
d.append({'column': key, 'value': value, 'type': 'string'})
|
|
elif isinstance(value, int):
|
|
d.append({'column': key, 'value': str(value), 'type': 'int'})
|
|
else:
|
|
raise NotImplementedError
|
|
payload['status'] = self.status
|
|
return payload
|
|
|
|
def get_serialize(self):
|
|
d = {'res_id': self.pk}
|
|
for key in self.__dict__:
|
|
if key in ['pk', 'maarch_courrier']:
|
|
continue
|
|
value = getattr(self, key)
|
|
if key == 'content':
|
|
value = base64.b64encode(value)
|
|
key = 'fileBase64Content'
|
|
if key.endswith('_date'):
|
|
value = value.isoformat()
|
|
d[key] = value
|
|
return d
|
|
|
|
def new_courrier_with_file(self, content, format, status, **kwargs):
|
|
return self.Courrier.new_with_file(self, content, format, status, **kwargs)
|
|
|
|
@property
|
|
def session(self):
|
|
s = requests.Session()
|
|
if self.username and self.password:
|
|
s.auth = (self.username, self.password)
|
|
retry = Retry(
|
|
total=self.max_retries,
|
|
read=self.max_retries,
|
|
connect=self.max_retries,
|
|
backoff_factor=0.5,
|
|
status_forcelist=(500, 502, 504),
|
|
)
|
|
adapter = HTTPAdapter(max_retries=retry)
|
|
s.mount('http://', adapter)
|
|
s.mount('https://', adapter)
|
|
return s
|
|
|
|
def post_json(self, url, payload, verb='post'):
|
|
try:
|
|
method = getattr(self.session, verb)
|
|
response = method(url, json=payload)
|
|
except requests.RequestException as e:
|
|
raise MaarchError('HTTP request to maarch failed', e, payload)
|
|
try:
|
|
response.raise_for_status()
|
|
except requests.RequestException as e:
|
|
raise MaarchError('HTTP request to maarch failed', e, payload, repr(response.content[:1000]))
|
|
try:
|
|
response_payload = response.json()
|
|
except ValueError:
|
|
raise MaarchError('maarch returned non-JSON data', repr(response.content[:1000]), payload)
|
|
return response_payload
|
|
|
|
def put_json(self, url, payload):
|
|
return self.post_json(url, payload, verb='put')
|
|
|
|
@property
|
|
def list_url(self):
|
|
return urlparse.urljoin(self.url, 'rest/res/list')
|
|
|
|
@property
|
|
def update_external_infos_url(self):
|
|
return urlparse.urljoin(self.url, 'rest/res/externalInfos')
|
|
|
|
@property
|
|
def update_status_url(self):
|
|
return urlparse.urljoin(self.url, 'rest/res/resource/status')
|
|
|
|
@property
|
|
def post_courrier_url(self):
|
|
return urlparse.urljoin(self.url, 'rest/res')
|
|
|
|
def get_courriers(self, clause, fields=None, limit=None, include_file=False, order_by=None):
|
|
if fields:
|
|
# res_id is mandatory
|
|
fields = set(fields)
|
|
fields.add('res_id')
|
|
fields = ','.join(fields) if fields else '*'
|
|
limit = limit or self.default_limit
|
|
order_by = order_by or []
|
|
response = self.post_json(
|
|
self.list_url,
|
|
{
|
|
'select': fields,
|
|
'clause': clause,
|
|
'limit': limit,
|
|
'withFile': include_file,
|
|
'orderBy': order_by,
|
|
},
|
|
)
|
|
if not hasattr(response.get('resources'), 'append'):
|
|
raise MaarchError('missing resources field or bad type', response)
|
|
return [self.Courrier(self, **resource) for resource in response['resources']]
|
|
|
|
def update_external_infos(self, courriers, status):
|
|
if not courriers:
|
|
return
|
|
external_infos = []
|
|
payload = {
|
|
'externalInfos': external_infos,
|
|
'status': status,
|
|
}
|
|
for courrier in courriers:
|
|
assert courrier.pk, 'courrier must already exist in Maarch and have a pk'
|
|
external_info = {'res_id': courrier.pk}
|
|
if getattr(courrier, 'external_id', None):
|
|
external_info['external_id'] = courrier.external_id
|
|
if getattr(courrier, 'external_link', None):
|
|
external_info['external_link'] = courrier.external_link
|
|
external_infos.append(external_info)
|
|
response = self.put_json(self.update_external_infos_url, payload)
|
|
if 'errors' in response:
|
|
raise MaarchError('update_external_infos failed with errors', response['errors'], response)
|
|
|
|
def update_status(self, courriers, status, history_message=None):
|
|
if not courriers:
|
|
return
|
|
res_ids = []
|
|
for courrier in courriers:
|
|
assert courrier.pk
|
|
res_ids.append(courrier.pk)
|
|
payload = {
|
|
'status': status,
|
|
'resId': res_ids,
|
|
}
|
|
if history_message:
|
|
payload['historyMessage'] = history_message
|
|
response = self.put_json(self.update_status_url, payload)
|
|
|
|
if 'errors' in response:
|
|
raise MaarchError('update_status failed with errors', response['errors'], response)
|
|
|
|
def post_courrier(self, courrier):
|
|
response = self.post_json(self.post_courrier_url, courrier.post_serialize())
|
|
if 'errors' in response:
|
|
raise MaarchError('update_external_infos failed with errors', response['errors'], response)
|
|
if 'resId' not in response:
|
|
raise MaarchError('update_external_infos failed with errors, missing resId', response)
|
|
courrier.pk = response['resId']
|
|
return courrier
|