[IMP] extracted the api part

This commit is contained in:
Guewen Baconnier @ Camptocamp
2012-11-23 10:18:09 +01:00
parent 9575e7a3a7
commit 8bb9c3bb77
7 changed files with 298 additions and 89 deletions

View File

@@ -20,4 +20,6 @@
############################################################################## ##############################################################################
import ir_attachment import ir_attachment
import pingen
import pingen_task import pingen_task

View File

@@ -49,6 +49,8 @@ Dependencies
* Require the Python library `requests <http://docs.python-requests.org/>`_ * Require the Python library `requests <http://docs.python-requests.org/>`_
* The PDF files sent to pingen.com have to respect some `formatting rules * The PDF files sent to pingen.com have to respect some `formatting rules
<https://stage-app.pingen.com/resources/pingen_requirements_v1_en.pdf>`_. <https://stage-app.pingen.com/resources/pingen_requirements_v1_en.pdf>`_.
* The address must be in a format accepted by pingen.com: the last line
is the country.
""", """,
'website': 'http://www.camptocamp.com', 'website': 'http://www.camptocamp.com',

View File

@@ -19,6 +19,9 @@
# #
############################################################################## ##############################################################################
import requests
import base64
from openerp.osv import osv, orm, fields from openerp.osv import osv, orm, fields
from openerp.tools.translate import _ from openerp.tools.translate import _
@@ -32,6 +35,19 @@ class ir_attachment(orm.Model):
'pingen_task_ids': fields.one2many( 'pingen_task_ids': fields.one2many(
'pingen.task', 'attachment_id', 'pingen.task', 'attachment_id',
string="Pingen Task", readonly=True), string="Pingen Task", readonly=True),
'pingen_send': fields.boolean(
'Send',
help="Defines if a document is merely uploaded or also sent"),
'pingen_speed': fields.selection(
[('1', 'Priority'), ('2', 'Economy')],
'Speed',
help="Defines the sending speed if the document is automatically sent"),
'pingen_color': fields.selection( [('0', 'B/W'), ('1', 'Color')], 'Type of print'),
}
_defaults = {
'pingen_send': True,
'pingen_speed': '2',
} }
def _prepare_pingen_task_vals(self, cr, uid, attachment, context=None): def _prepare_pingen_task_vals(self, cr, uid, attachment, context=None):
@@ -88,3 +104,20 @@ class ir_attachment(orm.Model):
self._handle_pingen_task(cr, uid, attachment_id, context=context) self._handle_pingen_task(cr, uid, attachment_id, context=context)
return res return res
def _decoded_content(self, cr, uid, attachment, context=None):
""" Returns the decoded content of an attachment (stored or url)
Returns None if the type is 'url' and the url is not reachable.
"""
decoded_document = None
if attachment.type == 'binary':
decoded_document = base64.decodestring(attachment.datas)
elif attachment.type == 'url':
response = requests.get(attachment.url)
if response.ok:
decoded_document = requests.content
else:
raise Exception(
'The type of attachment %s is not handled' % attachment.type)
return decoded_document

View File

@@ -11,6 +11,9 @@
<page string="Notes" position="before"> <page string="Notes" position="before">
<page string="Pingen.com"> <page string="Pingen.com">
<field name="send_to_pingen"/> <field name="send_to_pingen"/>
<field name="pingen_send" attrs="{'required': [('send_to_pingen', '=', True)]}"/>
<field name="pingen_speed" attrs="{'required': [('pingen_send', '=', True)]}"/>
<field name="pingen_color" />
</page> </page>
</page> </page>
</field> </field>

147
pingen/pingen.py Normal file
View File

@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 Camptocamp SA
#
# 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 requests
import logging
import urlparse
_logger = logging.getLogger(__name__)
class PingenException(RuntimeError):
"""There was an ambiguous exception that occurred while handling your
request."""
class ConnectionError(PingenException):
"""An Error occured with the pingen API"""
class APIError(PingenException):
"""An Error occured with the pingen API"""
class Pingen(object):
""" Interface to pingen.com API
"""
def __init__(self, token, staging=True):
self._token = token
self.staging = True
super(Pingen, self).__init__()
@property
def url(self):
if self.staging:
return 'https://stage-api.pingen.com'
return 'https://api.pingen.com'
def _send(self, method, endpoint, **kwargs):
""" Send a request to the pingen API using requests
Add necessary boilerplate to call pingen.com API
(authentication, configuration, ...)
:param boundmethod method: requests method to call
:param str endpoint: endpoint to call
:param kwargs: additional arguments forwarded to the requests method
"""
complete_url = urlparse.urljoin(self.url, endpoint)
auth_param = {'token': self._token}
if 'params' in kwargs:
kwargs['params'].update(auth_param)
else:
kwargs['params'] = auth_param
# with safe_mode, requests catch errors and
# returns a blank response with an error
config = {'safe_mode': True}
if 'config' in kwargs:
kwargs['config'].update(config)
else:
kwargs['config'] = config
# verify = False required for staging environment
# because the SSL certificate is wrong
kwargs['verify'] = not self.staging
response = method(complete_url, **kwargs)
if not response.ok:
raise ConnectionError(response.error)
if response.json['error']:
raise APIError(
"%s: %s" % (response.json['errorcode'], response.json['errormessage']))
return response
def push_document(self, document, send=None, speed=None, color=None):
""" Upload a document to pingen.com and eventually ask to send it
:param tuple document: (filename, file stream) to push
:param boolean send: if True, the document will be sent by pingen.com
:param int/str speed: sending speed of the document if it is send
1 = Priority, 2 = Economy
:param int/str color: type of print, 0 = B/W, 1 = Color
:return: tuple with 3 items:
1. document_id on pingen.com
2. post_id on pingen.com if it has been sent or None
3. dict of the created item on pingen (details)
"""
document = {'file': document}
data = {
'send': send,
'speed': speed,
'color': color,
}
response = self._send(
requests.post,
'document/upload',
data=data,
files=document)
json = response.json
document_id = json['id']
# confusing name but send_id is the posted id
posted_id = json.get('send', {}).get('send_id')
item = json['item']
return document_id, posted_id, item
def send_document(self, document_id, speed=None, color=None):
""" Send a uploaded document to pingen.com
:param int document_id: id of the document to send
:param int/str speed: sending speed of the document if it is send
1 = Priority, 2 = Economy
:param int/str color: type of print, 0 = B/W, 1 = Color
:return: id of the post on pingen.com
"""
response = self._send(
requests.post,
'document/send',
params={'id': document_id})
return response.json['id']

View File

@@ -19,17 +19,16 @@
# #
############################################################################## ##############################################################################
import requests
import urlparse
import base64
import logging import logging
import datetime
from cStringIO import StringIO from cStringIO import StringIO
from openerp.osv import orm, fields from openerp.osv import orm, fields
from openerp import tools
from .pingen import Pingen, APIError, ConnectionError
# TODO should be configurable # TODO should be configurable
BASE_URL = 'https://stage-api.pingen.com'
TOKEN = '6bc041af6f02854461ef31c2121ef853' TOKEN = '6bc041af6f02854461ef31c2121ef853'
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -48,28 +47,24 @@ class pingen_task(orm.Model):
_columns = { _columns = {
'attachment_id': fields.many2one( 'attachment_id': fields.many2one(
'ir.attachment', 'Document', required=True, ondelete='cascade'), 'ir.attachment', 'Document', required=True, readonly=True, ondelete='cascade'),
'state': fields.selection( 'state': fields.selection(
[('pending', 'Pending'), [('pending', 'Pending'),
('pushed', 'Pushed'), ('pushed', 'Pushed'),
('sent', 'Sent'),
('error', 'Error'), ('error', 'Error'),
('need_fix', 'Needs a correction'),
('canceled', 'Canceled')], ('canceled', 'Canceled')],
string='State', readonly=True, required=True), string='State', readonly=True, required=True),
'config': fields.text('Configuration (tmp)'),
'date': fields.datetime('Creation Date'), 'date': fields.datetime('Creation Date'),
'push_date': fields.datetime('Pushed Date'), 'push_date': fields.datetime('Push Date'),
'send': fields.boolean(
'Send',
help="Defines if a document is merely uploaed or also sent"),
'speed': fields.selection(
[(1, 'Priority'), (2, 'Economy')],
'Speed',
help="Defines the sending speed if the document is automatically sent"),
'color': fields.selection( [(0, 'B/W'), (1, 'Color')], 'Type of print'),
'last_error_code': fields.integer('Error Code', readonly=True),
'last_error_message': fields.text('Error Message', readonly=True), 'last_error_message': fields.text('Error Message', readonly=True),
'pingen_id': fields.integer('Pingen ID'), 'pingen_id': fields.integer(
'Pingen ID',
help="ID of the document in the Pingen Documents"),
'post_id': fields.integer(
'Pingen Post ID',
help="ID of the document in the Pingen Sendcenter"),
} }
_defaults = { _defaults = {
@@ -87,70 +82,47 @@ class pingen_task(orm.Model):
""" """
success = False attachment_obj = self.pool.get('ir.attachment')
push_url = urlparse.urljoin(BASE_URL, 'document/upload')
auth = {'token': TOKEN}
config = {
'send': False,
'speed': 2,
'color': 1,
}
task = self.browse(cr, uid, task_id, context=context) task = self.browse(cr, uid, task_id, context=context)
if task.type == 'binary': decoded_document = attachment_obj._decoded_content(
decoded_document = base64.decodestring(task.datas) cr, uid, task.attachment_id, context=context)
else:
url_resp = requests.get(task.url)
if url_resp.status_code != 200:
task.write({'last_error_code': False,
'last_error_message': "%s" % req.error,
'state': 'error'},
context=context)
return False
decoded_document = requests.content
document = {'file': (task.datas_fname, StringIO(decoded_document))} success = False
# parameterize
pingen = Pingen(TOKEN, staging=True)
doc = (task.datas_fname, StringIO(decoded_document))
try: try:
# TODO extract doc_id, post_id, __ = pingen.push_document(
req = requests.post( doc, task.pingen_send, task.pingen_speed, task.pingen_color)
push_url, except ConnectionError as e:
params=auth, # we can continue and it will be retried the next time
data=config, _logger.exception('Connection Error when pushing Pingen Task %s to %s.' %
files=document, (task.id, pingen.url))
# verify = False required for staging environment
verify=False)
req.raise_for_status()
except (requests.HTTPError,
requests.Timeout,
requests.ConnectionError) as e:
msg = ('%s Message: %s. \n'
'It occured when pushing the Pingen Task %s to pingen.com. \n'
'It will be retried later.' % (e.__doc__, e, task.id))
_logger.error(msg)
task.write( task.write(
{'last_error_code': False, {'last_error_message': e,
'last_error_message': msg,
'state': 'error'}, 'state': 'error'},
context=context) context=context)
else:
response = req.json except APIError as e:
if response['error']: _logger.warning('API Error when pushing Pingen Task %s to %s.' %
task.write( (task.id, pingen.url))
{'last_error_code': response['errorcode'], task.write(
'last_error_message': 'Error from pingen.com: %s' % response['errormessage'], {'last_error_message': e,
'state': 'error'}, 'state': 'need_fix'},
context=context)
else:
task.write(
{'last_error_code': False,
'last_error_message': False,
'state': 'pushed',
'pingen_id': response['id'],},
context=context) context=context)
_logger.info('Pingen Task %s pushed to pingen.com.' % task.id) else:
success = True success = True
now = datetime.datetime.now().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
task.write(
{'last_error_message': False,
'state': 'pushed',
'push_date': now,
'pingen_id': doc_id,
'post_id': post_id},
context=context)
_logger.info('Pingen Task %s pushed to %s' % (task.id, pingen.url))
return success return success
@@ -163,10 +135,3 @@ class pingen_task(orm.Model):
self._push_to_pingen(cr, uid, task_id, context=context) self._push_to_pingen(cr, uid, task_id, context=context)
return True return True
# r = requests.get(push_url, params=auth, verify=False)
# TODO: resolve = put in pending or put in pushed

View File

@@ -8,9 +8,12 @@
<field name="type">tree</field> <field name="type">tree</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Pingen Task"> <tree string="Pingen Task">
<field name="send"/> <field name="name"/>
<field name="speed"/> <field name="datas_fname"/>
<field name="color"/> <field name="pingen_send"/>
<field name="pingen_speed"/>
<field name="pingen_color"/>
<field name="state"/>
</tree> </tree>
</field> </field>
</record> </record>
@@ -21,11 +24,65 @@
<field name="type">form</field> <field name="type">form</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Pingen Task"> <form string="Pingen Task">
<field name="send"/> <group colspan="4" col="6">
<field name="speed"/> <field name="name" readonly="True"/>
<field name="color"/> <field name="type" readonly="True"/>
<field name="state"/> <field name="company_id" readonly="True" groups="base.group_multi_company" widget="selection"/>
<button name="push_to_pingen" states="pending" string="Push to pingen.com" icon="terp-camera_test"/> </group>
<notebook colspan="4">
<page string="Pingen.com">
<separator string="Options" colspan="4"/>
<newline />
<group col="2" colspan="2">
<field name="pingen_send"/>
<field name="pingen_speed" attrs="{'required': [('pingen_send', '=', True)]}"/>
<field name="pingen_color"/>
</group>
<separator string="Dates" colspan="4"/>
<newline />
<group col="2" colspan="2">
<field name="date"/>
<field name="push_date"/>
</group>
<separator string="Errors" colspan="4"/>
<newline />
<group col="2" colspan="2">
<field nolabel="1" name="last_error_message"/>
</group>
<separator string="Actions" colspan="4"/>
<newline />
<group col="2" colspan="2">
<button name="push_to_pingen" type="object"
states="pending,error,need_fix"
string="Push to pingen.com" icon="terp-camera_test"/>
</group>
</page>
<page string="Attachment">
<group col="4" colspan="4">
<separator string="Data" colspan="4"/>
<newline />
<group col="2" colspan="4" attrs="{'invisible':[('type','=','url')]}">
<field name="datas" filename="datas_fname" readonly="True"/>
<field name="datas_fname" select="1" readonly="True"/>
</group>
<group col="2" colspan="4" attrs="{'invisible':[('type','=','binary')]}">
<field name="url" widget="url" readonly="True"/>
</group>
</group>
<group col="2" colspan="4">
<separator string="Attached To" colspan="2"/>
<field name="attachment_id"/>
</group>
</page>
</notebook>
<field name="state" widget="statusbar"
statusbar_visible="pending,pushed,sent"
statusbar_colors='{"error":"red","need_fix":"red","canceled":"grey","pushed":"blue","sent":"green"}'/>
</form> </form>
</field> </field>
</record> </record>