diff --git a/CHANGELOG b/CHANGELOG index 7711a8b8bf..c48209a03d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add cache on RPC * Remove support for Python 3.4 * Allow to update record when importing data * Set context on record instantiated by Field.__set__ diff --git a/doc/ref/rpc.rst b/doc/ref/rpc.rst index c03594a8a4..6d13156441 100644 --- a/doc/ref/rpc.rst +++ b/doc/ref/rpc.rst @@ -5,7 +5,7 @@ RPC === -.. class:: RPC([readonly[, instantiate[, result[, check_access[, unique[, fresh_session]]]]]]) +.. class:: RPC([readonly[, instantiate[, result[, check_access[, unique[, fresh_session[, cache]]]]]]]) RPC is an object to define the behavior of Remote Procedure Call. @@ -35,3 +35,25 @@ Instance attributes are: .. attribute:: RPC.fresh_session If set, it requires a fresh session. Default is `False`. + +.. attribute:: RPC.cache + + A :class:`RPCCache` instance to compute the cache duration for the answer. + +======== +RPCCache +======== + +.. class:: RPCCache([days[, seconds]) + +Instance attributes are: + +.. attribute:: RPC.duration + + A timdelta instance. + +Instance methods are: + +.. method:: RCP.headers + + Returns a dictionary of the headers. diff --git a/trytond/ir/action.py b/trytond/ir/action.py index 74bdf46b3e..9c2e5d2723 100644 --- a/trytond/ir/action.py +++ b/trytond/ir/action.py @@ -139,7 +139,9 @@ class ActionKeyword(ModelSQL, ModelView): @classmethod def __setup__(cls): super(ActionKeyword, cls).__setup__() - cls.__rpc__.update({'get_keyword': RPC()}) + cls.__rpc__.update({ + 'get_keyword': RPC(cache=dict(days=1)), + }) @classmethod def __register__(cls, module_name): diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py index 7c33b2aa59..586a09d275 100644 --- a/trytond/ir/ui/view.py +++ b/trytond/ir/ui/view.py @@ -312,7 +312,7 @@ class ViewTreeState(ModelSQL, ModelView): super(ViewTreeState, cls).__setup__() cls.__rpc__.update({ 'set': RPC(readonly=False, check_access=False), - 'get': RPC(check_access=False), + 'get': RPC(check_access=False, cache=dict(days=1)), }) @classmethod diff --git a/trytond/model/model.py b/trytond/model/model.py index 0928b92367..508098b66e 100644 --- a/trytond/model/model.py +++ b/trytond/model/model.py @@ -35,8 +35,8 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta): def __setup__(cls): super(Model, cls).__setup__() cls.__rpc__ = { - 'default_get': RPC(), - 'fields_get': RPC(), + 'default_get': RPC(cache=dict(seconds=5 * 60)), + 'fields_get': RPC(cache=dict(days=1)), 'pre_validate': RPC(instantiate=0), } diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py index a72d8899a4..d0b9c5786c 100644 --- a/trytond/model/modelview.py +++ b/trytond/model/modelview.py @@ -118,8 +118,8 @@ class ModelView(Model): @classmethod def __setup__(cls): super(ModelView, cls).__setup__() - cls.__rpc__['fields_view_get'] = RPC() - cls.__rpc__['view_toolbar_get'] = RPC() + cls.__rpc__['fields_view_get'] = RPC(cache=dict(days=1)) + cls.__rpc__['view_toolbar_get'] = RPC(cache=dict(days=1)) cls.__rpc__['on_change'] = RPC(instantiate=0) cls.__rpc__['on_change_with'] = RPC(instantiate=0) cls._buttons = {} diff --git a/trytond/protocols/dispatcher.py b/trytond/protocols/dispatcher.py index db71842583..f4c3fb91f7 100644 --- a/trytond/protocols/dispatcher.py +++ b/trytond/protocols/dispatcher.py @@ -214,4 +214,7 @@ def _dispatch(request, pool, *args, **kwargs): context = {'_request': request.context} security.reset(pool.database_name, session, context=context) logger.debug('Result: %s', result) - return result + response = app.make_response(request, result) + if rpc.readonly and rpc.cache: + response.headers.extend(rpc.cache.headers()) + return response diff --git a/trytond/rpc.py b/trytond/rpc.py index 62306e44d6..f4d0f411cf 100644 --- a/trytond/rpc.py +++ b/trytond/rpc.py @@ -1,6 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import copy +import datetime as dt from trytond.transaction import Transaction @@ -19,10 +20,10 @@ class RPC(object): ''' __slots__ = ('readonly', 'instantiate', 'result', 'check_access', - 'fresh_session', 'unique') + 'fresh_session', 'unique', 'cache') def __init__(self, readonly=True, instantiate=None, result=None, - check_access=True, fresh_session=False, unique=True): + check_access=True, fresh_session=False, unique=True, cache=None): self.readonly = readonly self.instantiate = instantiate if result is None: @@ -31,6 +32,10 @@ class RPC(object): self.check_access = check_access self.fresh_session = fresh_session self.unique = unique + if cache: + if not isinstance(cache, RPCCache): + cache = RPCCache(**cache) + self.cache = cache def convert(self, obj, *args, **kwargs): args = list(args) @@ -70,3 +75,15 @@ class RPC(object): if self.check_access: context['_check_access'] = True return args, kwargs, context, timestamp + + +class RPCCache: + __slots__ = ('duration',) + + def __init__(self, days=0, seconds=0): + self.duration = dt.timedelta(days=days, seconds=seconds) + + def headers(self): + return { + 'X-Tryton-Cache': int(self.duration.total_seconds()), + } diff --git a/trytond/wsgi.py b/trytond/wsgi.py index 44379e0c61..8c1e9e4c5c 100644 --- a/trytond/wsgi.py +++ b/trytond/wsgi.py @@ -116,6 +116,27 @@ class TrytondWSGI(object): response = rv return response + def make_response(self, request, data): + for cls in self.protocols: + for mimetype, _ in request.accept_mimetypes: + if cls.content_type in mimetype: + response = cls.response(data, request) + break + else: + continue + break + else: + for cls in self.protocols: + if cls.content_type in request.environ.get('CONTENT_TYPE', ''): + response = cls.response(data, request) + break + else: + if isinstance(data, Exception): + response = InternalServerError(data) + else: + response = Response(data) + return response + def wsgi_app(self, environ, start_response): for cls in self.protocols: if cls.content_type in environ.get('CONTENT_TYPE', ''): @@ -135,24 +156,7 @@ class TrytondWSGI(object): data = self.dispatch_request(request) if not isinstance(data, (Response, HTTPException)): - for cls in self.protocols: - for mimetype, _ in request.accept_mimetypes: - if cls.content_type in mimetype: - response = cls.response(data, request) - break - else: - continue - break - else: - for cls in self.protocols: - if cls.content_type in environ.get('CONTENT_TYPE', ''): - response = cls.response(data, request) - break - else: - if isinstance(data, Exception): - response = InternalServerError(data) - else: - response = Response(data) + response = self.make_response(request, data) else: response = data @@ -167,8 +171,6 @@ class TrytondWSGI(object): response.headers['Access-Control-Allow-Headers'] = headers response.headers['Access-Control-Max-Age'] = config.getint( 'web', 'cache_timeout') - - # TODO custom process response return response(environ, start_response) def __call__(self, environ, start_response):