# 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 . import base64 from dateutil.parser import parse as parse_datetime from django.utils import six from django.utils.six.moves.urllib import parse as urlparse import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry class MaarchError(Exception): pass class MaarchCourrier(object): 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 '' % self.url class Courrier(object): 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 '' % ' '.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, six.string_types): 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