misc: add a cache duration option to webservice calls (#51359) #567

Merged
fpeters merged 1 commits from wip/51359-wscall-cache-option into main 2023-08-11 00:31:21 +02:00
3 changed files with 72 additions and 7 deletions

View File

@ -295,3 +295,34 @@ def test_webservice_timeout(http_requests, pub):
except Exception:
pass
assert http_requests.get_last('timeout') == 10
def test_webservice_cache(http_requests, pub):
NamedWsCall.wipe()
wscall = NamedWsCall()
wscall.name = 'Hello world'
wscall.request = {
'method': 'GET',
'url': 'http://remote.example.net/json',
'cache_duration': '120',
}
wscall.store()
wscall = NamedWsCall.get(wscall.id)
assert wscall.request['cache_duration'] == '120'
assert wscall.call() == {'foo': 'bar'}
assert http_requests.count() == 1
# remove request cache
pub.get_request().wscalls_cache = {}
assert wscall.call() == {'foo': 'bar'}
assert http_requests.count() == 1
# make request without cache
wscall.request = {
'method': 'GET',
'url': 'http://remote.example.net/json',
'cache_duration': None,
}
pub.get_request().wscalls_cache = {}
assert wscall.call() == {'foo': 'bar'}
assert http_requests.count() == 2

View File

@ -31,6 +31,9 @@
{% if wscall.request.request_signature_key %}
<li>{% trans "Request Signature Key:" %} {{ wscall.request.request_signature_key }}</li>
{% endif %}
{% if wscall.request.method == 'GET' and wscall.request.cache_duration %}
<li>{% trans "Cache duration:" %} {{ wscall.request.cache_duration }}s</li>
{% endif %}
{% if wscall.request.timeout %}
<li>{% trans "Timeout:" %} {{ wscall.request.timeout }}s</li>
{% endif %}

View File

@ -15,12 +15,14 @@
# along with this program; if not, see <http://www.gnu.org/licenses/>.
import collections
import hashlib
import json
import urllib.parse
import xml.etree.ElementTree as ET
from django.conf import settings
from django.utils.encoding import force_str
from django.core.cache import cache as django_cache
from django.utils.encoding import force_bytes, force_str
from quixote import get_publisher, get_request
from wcs.api_utils import MissingSecret, get_secret_and_orig, sign_url
@ -32,6 +34,7 @@ from .qommon.form import (
CheckboxWidget,
CompositeWidget,
ComputedExpressionWidget,
DurationWidget,
RadiobuttonsWidget,
StringWidget,
WidgetDict,
@ -64,6 +67,11 @@ def get_app_error_code(response, data, response_type):
return app_error_code
def get_cache_key(url):
cache_key = url
return force_str(hashlib.md5(force_bytes(cache_key)).hexdigest())
def call_webservice(
url,
qs_data=None,
@ -73,6 +81,7 @@ def call_webservice(
post_formdata=None,
formdata=None,
cache=False,
cache_duration=None,
timeout=None,
notify_on_errors=False,
record_on_errors=False,
@ -114,10 +123,16 @@ def call_webservice(
unsigned_url = url
if method == 'GET' and cache is True: # check cache
request = get_request()
if hasattr(request, 'wscalls_cache') and unsigned_url in request.wscalls_cache:
return (None,) + request.wscalls_cache[unsigned_url]
if method == 'GET':
if cache is True: # check request cache
request = get_request()
if hasattr(request, 'wscalls_cache') and unsigned_url in request.wscalls_cache:
return (None,) + request.wscalls_cache[unsigned_url]
if cache_duration and int(cache_duration):
cache_key = 'wscall-%s' % get_cache_key(unsigned_url)
cached_result = django_cache.get(cache_key)
if cached_result:
return (None,) + cached_result
if request_signature_key:
signature_key = str(WorkflowStatusItem.compute(request_signature_key))
@ -165,6 +180,9 @@ def call_webservice(
request = get_request()
if cache is True and request and hasattr(request, 'wscalls_cache'):
request.wscalls_cache[unsigned_url] = (status, data)
if cache_duration:
cache_key = 'wscall-%s' % get_cache_key(unsigned_url)
django_cache.set(cache_key, (status, data), int(cache_duration))
except ConnectionError as e:
if not handle_connection_errors:
raise e
@ -246,7 +264,7 @@ class NamedWsCall(XmlStorableObject):
def export_request_to_xml(self, element, attribute_name, charset, **kwargs):
request = getattr(self, attribute_name)
for attr in ('url', 'request_signature_key', 'method', 'timeout'):
for attr in ('url', 'request_signature_key', 'method', 'timeout', 'cache_duration'):
ET.SubElement(element, attr).text = force_str(request.get(attr) or '', charset)
for attr in ('qs_data', 'post_data'):
data_element = ET.SubElement(element, attr)
@ -259,7 +277,7 @@ class NamedWsCall(XmlStorableObject):
def import_request_from_xml(self, element, **kwargs):
request = {}
for attr in ('url', 'request_signature_key', 'method', 'timeout'):
for attr in ('url', 'request_signature_key', 'method', 'timeout', 'cache_duration'):
request[attr] = ''
if element.find(attr) is not None and element.find(attr).text:
request[attr] = force_str(element.find(attr).text)
@ -386,6 +404,18 @@ class WsCallRequestWidget(CompositeWidget):
if value and not value.isdecimal():
raise ValueError(_('Timeout must be empty or a number.'))
self.add(
DurationWidget,
'cache_duration',
value=value.get('cache_duration'),
title=_('Cache Duration'),
required=False,
attrs={
'data-dynamic-display-child-of': method_widget.get_name(),
'data-dynamic-display-value': methods.get('GET'),
},
)
self.add(
StringWidget,
'timeout',
@ -410,6 +440,7 @@ class WsCallRequestWidget(CompositeWidget):
'post_formdata',
'post_data',
'timeout',
'cache_duration',
):
if not self.include_post_formdata and name == 'post_formdata':
continue