welco/welco/sources/mail/maarch.py

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