Skip to content

Commit

Permalink
Open a Magento client only once per sync
Browse files Browse the repository at this point in the history
* Create a MagentoAPI instance at the beginning of the
  MagentoBackend.work_on()
* MagentoBackend.work_on() is now a contextmanager so we can close
  the magento client at the end of the session
* The magento.API instance is created the first time we need it and
  reused
* The magento_api attribute is available from the WorkContext which is
  passed transversally
  • Loading branch information
guewen committed Jun 20, 2017
1 parent 452271b commit 7463d1b
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 104 deletions.
24 changes: 12 additions & 12 deletions magentoerpconnect/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,33 @@ def import_batch(self, backend, filters=None):
""" Prepare the import of records modified on Magento """
if filters is None:
filters = {}
work = backend.work_on(self._name)
importer = work.component(usage='batch.importer')
return importer.run(filters=filters)
with backend.work_on(self._name) as work:
importer = work.component(usage='batch.importer')
return importer.run(filters=filters)

@job(default_channel='root.magento')
@related_action(action='related_action_magento_link')
@api.model
def import_record(self, backend, external_id, force=False):
""" Import a Magento record """
work = backend.work_on(self._name)
importer = work.component(usage='record.importer')
return importer.run(external_id, force=force)
with backend.work_on(self._name) as work:
importer = work.component(usage='record.importer')
return importer.run(external_id, force=force)

@job(default_channel='root.magento')
@related_action(action='related_action_unwrap_binding')
@api.multi
def export_record(self, fields=None):
""" Export a record on Magento """
self.ensure_one()
work = self.backend_id.work_on(self._name)
exporter = work.component(usage='record.exporter')
return exporter.run(self, fields)
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='record.exporter')
return exporter.run(self, fields)

@job(default_channel='root.magento')
@related_action(action='related_action_magento_link')
def export_delete_record(self, backend, external_id):
""" Delete a record on Magento """
work = backend.work_on(self._name)
deleter = work.component(usage='record.exporter.deleter')
return deleter.run(external_id)
with backend.work_on(self._name) as work:
deleter = work.component(usage='record.exporter.deleter')
return deleter.run(external_id)
136 changes: 80 additions & 56 deletions magentoerpconnect/components/backend_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,27 +85,85 @@ def location(self):
return location


class MagentoAPI(object):

def __init__(self, location):
"""
:param location: Magento location
:type location: :class:`MagentoLocation`
"""
self._location = location
self._api = None

@property
def api(self):
if self._api is None:
custom_url = self._location.use_custom_api_path
api = magentolib.API(
self._location.location,
self._location.username,
self._location.password,
full_url=custom_url
)
api.__enter__()
self._api = api
return self._api

def __enter__(self):
# we do nothing, api is lazy
return self

def __exit__(self, type, value, traceback):
if self._api is not None:
self._api.__exit__(type, value, traceback)

def call(self, method, arguments):
try:
# When Magento is installed on PHP 5.4+, the API
# may return garble data if the arguments contain
# trailing None.
if isinstance(arguments, list):
while arguments and arguments[-1] is None:
arguments.pop()
start = datetime.now()
try:
result = self.api.call(method, arguments)
except:
_logger.error("api.call('%s', %s) failed", method, arguments)
raise
else:
_logger.debug("api.call('%s', %s) returned %s in %s seconds",
method, arguments, result,
(datetime.now() - start).seconds)
# Uncomment to record requests/responses in ``recorder``
# record(method, arguments, result)
return result
except (socket.gaierror, socket.error, socket.timeout) as err:
raise NetworkRetryableError(
'A network error caused the failure of the job: '
'%s' % err)
except xmlrpclib.ProtocolError as err:
if err.errcode in [502, # Bad gateway
503, # Service unavailable
504]: # Gateway timeout
raise RetryableJobError(
'A protocol error caused the failure of the job:\n'
'URL: %s\n'
'HTTP/HTTPS headers: %s\n'
'Error code: %d\n'
'Error message: %s\n' %
(err.url, err.headers, err.errcode, err.errmsg))
else:
raise


class MagentoCRUDAdapter(AbstractComponent):
""" External Records Adapter for Magento """

_name = 'magento.crud.adapter'
_inherit = ['base.backend.adapter', 'base.magento.connector']
_usage = 'backend.adapter'

def __init__(self, work_context):
super(MagentoCRUDAdapter, self).__init__(work_context)
backend = self.backend_record
magento = MagentoLocation(
backend.location,
backend.username,
backend.password,
use_custom_api_path=backend.use_custom_api_path)
if backend.use_auth_basic:
magento.use_auth_basic = True
magento.auth_basic_username = backend.auth_basic_username
magento.auth_basic_password = backend.auth_basic_password
self.magento = magento

def search(self, filters=None):
""" Search records according to some criterias
and returns a list of ids """
Expand Down Expand Up @@ -134,48 +192,14 @@ def delete(self, id):

def _call(self, method, arguments):
try:
custom_url = self.magento.use_custom_api_path
_logger.debug("Start calling Magento api %s", method)
with magentolib.API(self.magento.location,
self.magento.username,
self.magento.password,
full_url=custom_url) as api:
# When Magento is installed on PHP 5.4+, the API
# may return garble data if the arguments contain
# trailing None.
if isinstance(arguments, list):
while arguments and arguments[-1] is None:
arguments.pop()
start = datetime.now()
try:
result = api.call(method, arguments)
except:
_logger.error("api.call(%s, %s) failed", method, arguments)
raise
else:
_logger.debug("api.call(%s, %s) returned %s in %s seconds",
method, arguments, result,
(datetime.now() - start).seconds)
# Uncomment to record requests/responses in ``recorder``
# record(method, arguments, result)
return result
except (socket.gaierror, socket.error, socket.timeout) as err:
raise NetworkRetryableError(
'A network error caused the failure of the job: '
'%s' % err)
except xmlrpclib.ProtocolError as err:
if err.errcode in [502, # Bad gateway
503, # Service unavailable
504]: # Gateway timeout
raise RetryableJobError(
'A protocol error caused the failure of the job:\n'
'URL: %s\n'
'HTTP/HTTPS headers: %s\n'
'Error code: %d\n'
'Error message: %s\n' %
(err.url, err.headers, err.errcode, err.errmsg))
else:
raise
magento_api = getattr(self.work, 'magento_api')
except AttributeError:
raise AttributeError(
'You must provide a magento_api attribute with a '
'MagentoAPI instance to be able to use the '
'Backend Adapter.'
)
return magento_api.call(method, arguments)


class GenericAdapter(AbstractComponent):
Expand Down
14 changes: 7 additions & 7 deletions magentoerpconnect/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ def delay_unlink(env, model_name, record_id):
Called on binding records."""
record = env[model_name].browse(record_id)
work = record.backend_id.work_on(model_name)
binder = work.component(usage='binder')
external_id = binder.to_external(record_id)
if external_id:
binding = env[model_name].browse(record_id)
binding.with_delay().export_delete_record(record.backend_id,
external_id)
with record.backend_id.work_on(model_name) as work:
binder = work.component(usage='binder')
external_id = binder.to_external(record_id)
if external_id:
binding = env[model_name].browse(record_id)
binding.with_delay().export_delete_record(record.backend_id,
external_id)
6 changes: 3 additions & 3 deletions magentoerpconnect/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ class MagentoAccountInvoice(models.Model):
def export_invoice(self):
""" Export a validated or paid invoice. """
self.ensure_one()
work = self.backend_id.work_on(self._name)
exporter = work.component(usage='record.exporter')
return exporter.run(self)
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='record.exporter')
return exporter.run(self)


class AccountInvoice(models.Model):
Expand Down
21 changes: 20 additions & 1 deletion magentoerpconnect/magento_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import logging

from contextlib import contextmanager

from datetime import datetime, timedelta
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.addons.component.core import Component

from odoo.addons.connector.components.mapper import mapping
from odoo.addons.connector.checkpoint import checkpoint
from .components.backend_adapter import MagentoLocation, MagentoAPI

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -173,13 +177,28 @@ def check_magento_structure(self):
backend.synchronize_metadata()
return True

@contextmanager
@api.multi
def work_on(self, model_name, **kwargs):
self.ensure_one()
lang = self.default_lang_id
if lang.code != self.env.context.get('lang'):
self = self.with_context(lang=lang.code)
return super(MagentoBackend, self).work_on(model_name, **kwargs)
magento_location = MagentoLocation(
self.location,
self.username,
self.password,
use_custom_api_path=self.use_custom_api_path
)
# We create a Magento Client API here, so we can create the
# client once (lazily on the first use) and propagate it
# through all the sync session, instead of recreating a client
# in each backend adapter usage.
with MagentoAPI(magento_location) as magento_api:
# add it so we'll be able to use 'self.work.magento_api' in
# Components
kwargs['magento_api'] = magento_api
yield super(MagentoBackend, self).work_on(model_name, **kwargs)

@api.multi
def add_checkpoint(self, record):
Expand Down
18 changes: 9 additions & 9 deletions magentoerpconnect/models/queue_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ def related_action_magento_link(self, backend_id_pos=0, external_id_pos=1):
model_name = self.model_name
backend = self.args[backend_id_pos]
external_id = self.args[external_id_pos]
work = backend.work_on(model_name)
adapter = work.component(usage='backend.adapter')
try:
url = adapter.admin_url(external_id)
except ValueError:
raise exceptions.UserError(
_('No admin URL configured on the backend or '
'no admin path is defined for this record.')
)
with backend.work_on(model_name) as work:
adapter = work.component(usage='backend.adapter')
try:
url = adapter.admin_url(external_id)
except ValueError:
raise exceptions.UserError(
_('No admin URL configured on the backend or '
'no admin path is defined for this record.')
)

action = {
'type': 'ir.actions.act_url',
Expand Down
6 changes: 3 additions & 3 deletions magentoerpconnect/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ def product_type_get(self):
def export_inventory(self, fields=None):
""" Export the inventory configuration and quantity of a product. """
self.ensure_one()
work = self.backend_id.work_on(self._name)
exporter = work.component(usage='product.inventory.exporter')
return exporter.run(self, fields)
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='product.inventory.exporter')
return exporter.run(self, fields)

@api.multi
def recompute_magento_qty(self):
Expand Down
8 changes: 4 additions & 4 deletions magentoerpconnect/sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ def export_state_change(self, allowed_states=None,
comment=None, notify=None):
""" Change state of a sales order on Magento """
self.ensure_one()
work = self.backend_id.work_on(self._name)
exporter = work.component(usage='sale.state.exporter')
return exporter.run(self, allowed_states=allowed_states,
comment=comment, notify=notify)
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='sale.state.exporter')
return exporter.run(self, allowed_states=allowed_states,
comment=comment, notify=notify)

@job(default_channel='root.magento')
@api.model
Expand Down
18 changes: 9 additions & 9 deletions magentoerpconnect/stock_picking.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ class MagentoStockPicking(models.Model):
def export_tracking_number(self):
""" Export the tracking number of a delivery order. """
self.ensure_one()
work = self.backend_id.work_on(self._name)
exporter = work.component(usage='tracking.exporter')
return exporter.run(self)
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='tracking.exporter')
return exporter.run(self)

@job(default_channel='root.magento')
@related_action(action='related_action_unwrap_binding')
Expand All @@ -53,12 +53,12 @@ def export_picking_done(self, with_tracking=True):
# it should be called with True only if the carrier_tracking_ref
# is True when the job is created.
self.ensure_one()
work = self.backend_id.work_on(self._name)
exporter = work.component(usage='record.exporter')
res = exporter.run(self)
if with_tracking and self.carrier_tracking_ref:
self.with_delay().export_tracking_number()
return res
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='record.exporter')
res = exporter.run(self)
if with_tracking and self.carrier_tracking_ref:
self.with_delay().export_tracking_number()
return res


class StockPicking(models.Model):
Expand Down

0 comments on commit 7463d1b

Please sign in to comment.