Page MenuHomeSoftware Heritage

D507.id1570.diff
No OneTemporary

D507.id1570.diff

diff --git a/swh/core/api.py b/swh/core/api.py
--- a/swh/core/api.py
+++ b/swh/core/api.py
@@ -4,6 +4,8 @@
# See top-level LICENSE file for more information
import collections
+import functools
+import inspect
import json
import logging
import pickle
@@ -19,6 +21,15 @@
pass
+class RemotelyAccessibleStorage:
+ @staticmethod
+ def endpoint(path):
+ def dec(f):
+ f._endpoint_path = path
+ return f
+ return dec
+
+
class SWHRemoteAPI:
"""Proxy to an internal SWH API
@@ -104,6 +115,39 @@
return decode_response(response)
+def autogen_client_api(db_class):
+ """Decorator for SWHRemoteAPI, which adds a method for each endpoint
+ of the database it is designed to access."""
+ def decorator(client_api_class):
+ # For each method wrapped with @endpoint in IndexerStorage, add a new
+ # method in RemoteStorage, with the same documentation.
+ #
+ # Note that, despite the usage of decorator magic (eg. functools.wrap),
+ # this never actually calls an IndexerStorage method.
+ for (name, meth) in db_class.__dict__.items():
+ if hasattr(meth, '_endpoint_path'):
+ def _closure(name, meth):
+ wrapped_meth = inspect.unwrap(meth)
+
+ @functools.wraps(meth) # Copy signature and doc
+ def meth_(*args, **kwargs):
+ # Match arguments and parameters
+ post_data = inspect.getcallargs(
+ wrapped_meth, *args, **kwargs)
+
+ # Remove arguments that should not be passed
+ self = post_data.pop('self')
+ post_data.pop('cur', None)
+ post_data.pop('db', None)
+
+ # Send the request.
+ return self.post(meth._endpoint_path, post_data)
+ setattr(client_api_class, name, meth_)
+ _closure(name, meth)
+ return client_api_class
+ return decorator
+
+
class BytesRequest(Request):
"""Request with proper escaping of arbitrary byte sequences."""
encoding = 'utf-8'
@@ -143,3 +187,24 @@
class SWHServerAPIApp(Flask):
request_class = BytesRequest
+
+
+def register_db_endpoints(app, storage_class, storage_factory):
+ """For each endpoint of the given database, calls app.route to call
+ a function that decodes the request and sends it to the database object
+ provided by the factory."""
+ from flask import request
+
+ # See autogen_client_api to understand this magic
+ for (name, meth) in storage_class.__dict__.items():
+ if hasattr(meth, '_endpoint_path'):
+ def closure(name, meth):
+ @app.route('/'+meth._endpoint_path, methods=['POST'])
+ @functools.wraps(meth) # Copy signature and doc
+ def _f():
+ # Call the actual code
+ obj_meth = getattr(storage_factory(), name)
+ return encode_data_server(
+ obj_meth(**decode_request(request)))
+ globals()[name] = _f
+ closure(name, meth)
diff --git a/swh/core/tests/test_api.py b/swh/core/tests/test_api.py
new file mode 100644
--- /dev/null
+++ b/swh/core/tests/test_api.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2018 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+import unittest
+from nose.tools import istest
+
+import requests_mock
+from werkzeug.wrappers import BaseResponse
+from werkzeug.test import Client as WerkzeugTestClient
+
+from swh.core.api import (
+ register_db_endpoints, error_handler, encode_data_server,
+ RemotelyAccessibleStorage, SWHRemoteAPI,
+ autogen_client_api, SWHServerAPIApp)
+
+
+class ApiTest(unittest.TestCase):
+ @istest
+ def test_server(self):
+ testcase = self
+ nb_endpoint_calls = 0
+
+ class TestStorage(RemotelyAccessibleStorage):
+ @RemotelyAccessibleStorage.endpoint('test_endpoint_url')
+ def test_endpoint(self, test_data, db=None, cur=None):
+ nonlocal nb_endpoint_calls
+ nb_endpoint_calls += 1
+
+ testcase.assertEqual(test_data, 'spam')
+ return 'egg'
+
+ app = SWHServerAPIApp('testapp')
+ register_db_endpoints(app, TestStorage, lambda: TestStorage())
+
+ @app.errorhandler(Exception)
+ def my_error_handler(exception):
+ return error_handler(exception, encode_data_server)
+
+ client = WerkzeugTestClient(app, BaseResponse)
+ res = client.post('/test_endpoint_url',
+ headers={'Content-Type': 'application/x-msgpack'},
+ data=b'\x81\xa9test_data\xa4spam')
+
+ self.assertEqual(nb_endpoint_calls, 1)
+ self.assertEqual(b''.join(res.response), b'\xa3egg')
+
+ @istest
+ def test_client(self):
+ class TestStorage(RemotelyAccessibleStorage):
+ @RemotelyAccessibleStorage.endpoint('test_endpoint_url')
+ def test_endpoint(self, test_data, db=None, cur=None):
+ pass
+
+ nb_http_calls = 0
+
+ def callback(request, context):
+ nonlocal nb_http_calls
+ nb_http_calls += 1
+ self.assertEqual(request.headers['Content-Type'],
+ 'application/x-msgpack')
+ self.assertEqual(request.body, b'\x81\xa9test_data\xa4spam')
+ context.headers['Content-Type'] = 'application/x-msgpack'
+ context.content = b'\xa3egg'
+ return b'\xa3egg'
+
+ adapter = requests_mock.Adapter()
+ adapter.register_uri('POST',
+ 'mock://example.com/test_endpoint_url',
+ content=callback)
+
+ @autogen_client_api(TestStorage)
+ class Testclient(SWHRemoteAPI):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.session.mount('mock', adapter)
+
+ c = Testclient('foo', 'mock://example.com/')
+ res = c.test_endpoint('spam')
+
+ self.assertEqual(nb_http_calls, 1)
+ self.assertEqual(res, 'egg')

File Metadata

Mime Type
text/plain
Expires
Fri, Jun 20, 5:49 PM (1 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3233269

Event Timeline