diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index bbf9215f..df6e86df 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,3 +1,4 @@
+
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
index 4762b6f3..280c02d4 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -352,4 +352,4 @@ exclude-protected=_asdict,_fields,_replace,_source,_make
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
-overgeneral-exceptions=Exception
+overgeneral-exceptions=builtins.Exception
diff --git a/examples/minimal/minimal.py b/examples/minimal/minimal.py
index cc86dac5..7aafe070 100755
--- a/examples/minimal/minimal.py
+++ b/examples/minimal/minimal.py
@@ -72,6 +72,11 @@ def notify(_string: Optional[str] = None) -> None:
pass
+@jsonrpc.method('App.notNotify', notification=False)
+def not_notify(string: str) -> str:
+ return f'Not allow notification: {string}'
+
+
@jsonrpc.method('App.fails')
def fails(_string: Optional[str] = None) -> NoReturn:
raise ValueError('example of fail')
diff --git a/src/flask_jsonrpc/app.py b/src/flask_jsonrpc/app.py
index 3103c388..2fb9cdb7 100644
--- a/src/flask_jsonrpc/app.py
+++ b/src/flask_jsonrpc/app.py
@@ -75,10 +75,8 @@ def init_app(self, app: Flask) -> None:
)
self.register_browse(app, self)
- def register(
- self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, validate: bool = True, **options: t.Any
- ) -> None:
- self.register_view_function(view_func, name, validate, **options)
+ def register(self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, **options: t.Any) -> None:
+ self.register_view_function(view_func, name, **options)
def register_blueprint(
self, app: Flask, jsonrpc_app: 'JSONRPCBlueprint', url_prefix: str, enable_web_browsable_api: bool = False
diff --git a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js
index 5fcd9f30..4889637c 100644
--- a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js
+++ b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/controllers.js
@@ -16,9 +16,10 @@
$scope.showFakeIntro = true;
$scope.showContentLoaded = true;
$scope.showToolbar = false;
+ $scope.showToolbarNotifyButton = true;
$scope.breadcrumbs = breadcrumbs('Dashboard');
$scope.response = responseExample;
- $scope.response_object = responseObjectExample;
+ $scope.responseObject = responseObjectExample;
$scope.$on('App:displayFakeIntro', function(event, display) {
$scope.showFakeIntro = display;
@@ -36,6 +37,10 @@
$scope.showToolbar = display;
});
+ $scope.$on('App:displayToolbarNotifyButton', function(event, display) {
+ $scope.showToolbarNotifyButton = display;
+ });
+
$scope.showSpinner = function() {
return PendingRequests.isPending();
};
@@ -67,11 +72,17 @@
}, 750);
});
+ $scope.goToDashboard = function() {
+ $scope.$emit('App:displayToolbar', false);
+ $scope.$emit('App:breadcrumb', 'Dashboard');
+ $location.path('/');
+ };
+
$scope.showTooltip = function(module) {
return Handlebars.template('menu-module-tooltip', module);
};
- $scope.showReponseObject = function(module) {
+ $scope.showResponseObject = function(module) {
return $location.path(module.name);
};
}]);
@@ -98,7 +109,7 @@
};
$scope.hitEnter = function(evt) {
- if (angular.equals(evt.keyCode,13) && !(angular.equals($scope.name,null) || angular.equals($scope.name,''))) {
+ if (angular.equals(evt.keyCode, 13) && !(angular.equals($scope.name, null) || angular.equals($scope.name, ''))) {
$scope.ok();
}
};
@@ -112,25 +123,26 @@
$scope.module = module;
$scope.$emit('App:displayToolbar', true);
$scope.$emit('App:breadcrumb', module.name);
+ $scope.$emit('App:displayToolbarNotifyButton', module.options.notification);
var RPCCall = function(module) {
var payload = RPC.payload(module);
- $scope.request_object = payload;
+ $scope.requestObject = payload;
$scope.response = undefined;
- $scope.response_object = undefined;
- RPC.callWithPayload(payload).success(function(response_object, status, headers, config) { // success
- var headers_pretty = headers();
- headers_pretty.data = config.data;
+ $scope.responseObject = undefined;
+ RPC.callWithPayload(payload).success(function(responseObject, status, headers, config) { // success
+ var headersPretty = headers();
+ headersPretty.data = config.data;
- $scope.response = {status: status, headers: headers_pretty, config: config};
- $scope.response_object = response_object;
+ $scope.response = {status: status, headers: headersPretty, config: config};
+ $scope.responseObject = responseObject;
$scope.$emit('App:displayContentLoaded', false);
- }).error(function(response_object, status, headers, config) { // error
- var headers_pretty = headers();
- headers_pretty.data = config.data;
+ }).error(function(responseObject, status, headers, config) { // error
+ var headersPretty = headers();
+ headersPretty.data = config.data;
- $scope.response = {status_code: status, headers: headers_pretty, config: config};
- $scope.response_object = response_object;
+ $scope.response = {statusCode: status, headers: headersPretty, config: config};
+ $scope.responseObject = responseObject;
$scope.$emit('App:displayContentLoaded', false);
});
},
diff --git a/src/flask_jsonrpc/contrib/browse/templates/browse/index.html b/src/flask_jsonrpc/contrib/browse/templates/browse/index.html
index c55b894b..547e7e88 100644
--- a/src/flask_jsonrpc/contrib/browse/templates/browse/index.html
+++ b/src/flask_jsonrpc/contrib/browse/templates/browse/index.html
@@ -30,7 +30,7 @@
@@ -38,14 +38,14 @@
{% endraw %}
diff --git a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html
index 9b57a5e9..4002c144 100644
--- a/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html
+++ b/src/flask_jsonrpc/contrib/browse/templates/browse/partials/response_object.html
@@ -14,7 +14,9 @@ Summary: None{{param.name}} -> {{param.type}}:
-
+
+
+
@@ -36,12 +38,12 @@ Summary: None
{{ response.config.method }} {{ response.config.url }}
-
+
Response:
-
HTTP {{ response.status_code }}{{ response.status }}
-
+
HTTP {{ response.statusCode }}{{ response.status }}
+
diff --git a/src/flask_jsonrpc/settings.py b/src/flask_jsonrpc/settings.py
new file mode 100644
index 00000000..e2e89181
--- /dev/null
+++ b/src/flask_jsonrpc/settings.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2023-2023, Cenobit Technologies, Inc. http://cenobit.es/
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of the Cenobit Technologies nor the names of
+# its contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+import typing as t
+
+DEFAULTS = {
+ 'DEFAULT_JSONRPC_METHOD': {
+ 'VALIDATE': True,
+ 'NOTIFICATION': True,
+ },
+}
+
+
+class JSONRPCSettings:
+ def __init__(self, defaults: t.Optional[t.Dict[str, t.Any]] = None):
+ self.defaults = defaults or DEFAULTS
+
+ def __getattr__(self, attr: str) -> t.Any:
+ if attr not in self.defaults:
+ raise AttributeError(f"Invalid setting: '{attr}'")
+
+ val = self.defaults[attr]
+
+ setattr(self, attr, val)
+ return val
+
+
+settings = JSONRPCSettings(DEFAULTS)
diff --git a/src/flask_jsonrpc/site.py b/src/flask_jsonrpc/site.py
index 265ff457..502a047b 100644
--- a/src/flask_jsonrpc/site.py
+++ b/src/flask_jsonrpc/site.py
@@ -33,6 +33,7 @@
from werkzeug.datastructures import Headers
from .helpers import get, from_python_type
+from .settings import settings
from .exceptions import (
ParseError,
ServerError,
@@ -52,6 +53,7 @@
'ServiceProcedureDescribe',
{
'name': str,
+ 'options': t.Dict[str, t.Any],
'summary': t.Optional[str],
'params': t.List[t.Dict[str, str]],
'return': t.Dict[str, str],
@@ -173,9 +175,20 @@ def dispatch(
self, req_json: t.Dict[str, t.Any]
) -> t.Tuple[t.Any, int, t.Union[Headers, t.Dict[str, str], t.Tuple[str], t.List[t.Tuple[str]]]]:
params = req_json.get('params', {})
- view_func = self.view_funcs.get(req_json['method'])
+ method_name = req_json['method']
+ view_func = self.view_funcs.get(method_name)
+ validate = getattr(view_func, 'jsonrpc_validate', settings.DEFAULT_JSONRPC_METHOD['VALIDATE'])
+ notification = getattr(view_func, 'jsonrpc_notification', settings.DEFAULT_JSONRPC_METHOD['NOTIFICATION'])
if not view_func:
- raise MethodNotFoundError(data={'message': f"Method not found: {req_json['method']}"})
+ raise MethodNotFoundError(data={'message': f"Method not found: {method_name}"})
+
+ if self.is_notification_request(req_json) and not notification:
+ raise InvalidRequestError(
+ data={
+ 'message': f"The method '{method_name}' doesn't allow Notification "
+ "Request object (without an 'id' member)"
+ }
+ )
try:
if isinstance(params, (tuple, set, list)):
@@ -193,7 +206,7 @@ def dispatch(
# TODO: Improve the checker to return type
view_fun_annotations = t.get_type_hints(view_func)
view_fun_return: t.Optional[t.Any] = view_fun_annotations.pop('return', None)
- if resp_view is not None and view_fun_return is None:
+ if validate and resp_view is not None and view_fun_return is None:
resp_view_qn = qualified_name(resp_view)
view_fun_return_qn = qualified_name(view_fun_return)
raise TypeError(f'return type of {resp_view_qn} must be a type; got {view_fun_return_qn} instead')
@@ -258,6 +271,7 @@ def procedure_desc(self, key: str) -> ServiceProcedureDescribe: # pytype: disab
return {
'name': getattr(view_func, 'jsonrpc_method_name', key),
'summary': getattr(view_func, '__doc__', None),
+ 'options': getattr(view_func, 'jsonrpc_options', {}),
'params': [
{'name': k, 'type': self.python_type_name(t)}
for k, t in getattr(view_func, 'jsonrpc_method_params', {}).items()
diff --git a/src/flask_jsonrpc/wrappers.py b/src/flask_jsonrpc/wrappers.py
index 0ed9637b..46d90f6c 100644
--- a/src/flask_jsonrpc/wrappers.py
+++ b/src/flask_jsonrpc/wrappers.py
@@ -29,12 +29,21 @@
from typeguard import typechecked
+from .settings import settings
+
if t.TYPE_CHECKING:
from .site import JSONRPCSite
from .views import JSONRPCView
class JSONRPCDecoratorMixin:
+ def _method_options(self, options: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
+ default_options = {
+ 'validate': settings.DEFAULT_JSONRPC_METHOD['VALIDATE'],
+ 'notification': settings.DEFAULT_JSONRPC_METHOD['NOTIFICATION'],
+ }
+ return {**default_options, **options}
+
def _method_has_parameters(self, fn: t.Callable[..., t.Any]) -> bool:
fn_signature = signature(fn)
return bool(fn_signature.parameters)
@@ -68,26 +77,30 @@ def get_jsonrpc_site_api(self) -> t.Type['JSONRPCView']:
raise NotImplementedError
def register_view_function(
- self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, validate: bool = True, **options: t.Any
+ self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, **options: t.Dict[str, t.Any]
) -> t.Callable[..., t.Any]:
fn = self._get_function(view_func)
+ fn_options = self._method_options(options)
fn_annotations = t.get_type_hints(fn)
method_name = getattr(fn, '__name__', '') if not name else name
- view_func_wrapped = typechecked(view_func) if validate else view_func
+ view_func_wrapped = typechecked(view_func) if fn_options['validate'] else view_func
setattr(view_func_wrapped, 'jsonrpc_method_name', method_name) # noqa: B010
setattr(view_func_wrapped, 'jsonrpc_method_sig', fn_annotations) # noqa: B010
setattr(view_func_wrapped, 'jsonrpc_method_return', fn_annotations.pop('return', None)) # noqa: B010
setattr(view_func_wrapped, 'jsonrpc_method_params', fn_annotations) # noqa: B010
- setattr(view_func_wrapped, 'jsonrpc_validate', validate) # noqa: B010
- setattr(view_func_wrapped, 'jsonrpc_options', options) # noqa: B010
+ setattr(view_func_wrapped, 'jsonrpc_validate', fn_options['validate']) # noqa: B010
+ setattr(view_func_wrapped, 'jsonrpc_notification', fn_options['notification']) # noqa: B010
+ setattr(view_func_wrapped, 'jsonrpc_options', fn_options) # noqa: B010
self.get_jsonrpc_site().register(method_name, view_func_wrapped)
return view_func_wrapped
- def method(self, name: t.Optional[str] = None, validate: bool = True, **options: t.Any) -> t.Callable[..., t.Any]:
+ def method(self, name: t.Optional[str] = None, **options: t.Dict[str, t.Any]) -> t.Callable[..., t.Any]:
+ validate = options.get('validate', settings.DEFAULT_JSONRPC_METHOD['VALIDATE'])
+
def decorator(fn: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
method_name = getattr(fn, '__name__', '') if not name else name
if validate and not self._validate(fn):
raise ValueError(f'no type annotations present to: {method_name}')
- return self.register_view_function(fn, name, validate, **options)
+ return self.register_view_function(fn, name, **options)
return decorator
diff --git a/tests/contrib/test_browse.py b/tests/contrib/test_browse.py
index 626bdc4e..3e870bc2 100644
--- a/tests/contrib/test_browse.py
+++ b/tests/contrib/test_browse.py
@@ -34,15 +34,48 @@ def test_browse_create():
jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True)
# pylint: disable=W0612
- @jsonrpc.method('app.fn2')
- def fn1(s: str) -> str:
+ @jsonrpc.method('app.fn1', validate=False)
+ def fn1(s):
+ return f'Foo {s}'
+
+ # pylint: disable=W0612
+ @jsonrpc.method('app.fn2', notification=True)
+ def fn2(s: str) -> str:
+ return f'Foo {s}'
+
+ # pylint: disable=W0612
+ @jsonrpc.method('app.fn3', notification=False)
+ def fn3(s: str) -> str:
return f'Foo {s}'
with app.test_client() as client:
+ rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn1', 'params': [1]})
+ assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo 1'}
+ assert rv.status_code == 200
+
rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn2', 'params': [':)']})
assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'}
assert rv.status_code == 200
+ rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn3', 'params': [':)']})
+ assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'}
+ assert rv.status_code == 200
+
+ rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'app.fn3', 'params': [':)']})
+ assert rv.json == {
+ 'error': {
+ 'code': -32600,
+ 'data': {
+ 'message': "The method 'app.fn3' doesn't allow Notification Request object (without an 'id' member)"
+ },
+ 'message': 'Invalid Request',
+ 'name': 'InvalidRequestError',
+ },
+ 'id': None,
+ 'jsonrpc': '2.0',
+ }
+ assert rv.status_code == 400
+
rv = client.get('/api/browse')
assert rv.status_code == 308
@@ -53,12 +86,27 @@ def fn1(s: str) -> str:
rv = client.get('/api/browse/packages.json')
assert rv.json == {
'app': [
+ {
+ 'name': 'app.fn1',
+ 'options': {'notification': True, 'validate': False},
+ 'params': [],
+ 'return': {'type': 'Null'},
+ 'summary': None,
+ },
{
'name': 'app.fn2',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
- }
+ },
+ {
+ 'name': 'app.fn3',
+ 'options': {'notification': False, 'validate': True},
+ 'params': [{'name': 's', 'type': 'String'}],
+ 'return': {'type': 'String'},
+ 'summary': None,
+ },
]
}
assert rv.status_code == 200
@@ -66,6 +114,7 @@ def fn1(s: str) -> str:
rv = client.get('/api/browse/app.fn2.json')
assert rv.json == {
'name': 'app.fn2',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
@@ -102,6 +151,7 @@ def fn1(s: str) -> str:
'app': [
{
'name': 'app.fn2',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
@@ -150,12 +200,14 @@ def fn2(s: str) -> str:
'app': [
{
'name': 'app.fn2',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'app.fn3',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
@@ -177,12 +229,14 @@ def fn2(s: str) -> str:
'app': [
{
'name': 'app.fn1',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'app.fn2',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
@@ -220,12 +274,17 @@ def fn1_b2(s: str) -> str:
def fn2_b2(s: str) -> str:
return f'b2: Bar {s}'
+ # pylint: disable=W0612
+ @jsonrpc_api_2.method('blue2.not_notify', notification=False)
+ def fn3_b2(s: str) -> str:
+ return f'fn3 b2: Foo {s}'
+
jsonrpc_api_3 = JSONRPCBlueprint('jsonrpc_api_3', __name__)
# pylint: disable=W0612
@jsonrpc_api_3.method('blue3.fn2')
def fn1_b3(s: str) -> str:
- return f'b3: Foo {s}'
+ return f'fn1 b3: Foo {s}'
app = Flask('test_browse', instance_relative_config=True)
jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True)
@@ -239,6 +298,7 @@ def fn1_b3(s: str) -> str:
'blue1': [
{
'name': 'blue1.fn2',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
@@ -260,12 +320,21 @@ def fn1_b3(s: str) -> str:
'blue2': [
{
'name': 'blue2.fn1',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'blue2.fn2',
+ 'options': {'notification': True, 'validate': True},
+ 'params': [{'name': 's', 'type': 'String'}],
+ 'return': {'type': 'String'},
+ 'summary': None,
+ },
+ {
+ 'name': 'blue2.not_notify',
+ 'options': {'notification': False, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
diff --git a/tests/test_app.py b/tests/test_app.py
index 5d7080e9..76166b05 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -64,6 +64,11 @@ def fn2(s: str) -> str:
def fn3(s: str) -> str:
return f'Foo {s}'
+ # pylint: disable=W0612
+ @jsonrpc.method('app.fn4', notification=False)
+ def fn4(s: str) -> str:
+ return f'Goo {s}'
+
jsonrpc.register(fn3, name='app.fn3')
with app.test_client() as client:
@@ -87,6 +92,26 @@ def fn3(s: str) -> str:
assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'}
assert rv.status_code == 200
+ rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']})
+ assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Goo :)'}
+ assert rv.status_code == 200
+
+ rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']})
+ assert rv.json == {
+ 'error': {
+ 'code': -32600,
+ 'data': {
+ 'message': "The method 'app.fn4' doesn't allow Notification Request "
+ "object (without an 'id' member)"
+ },
+ 'message': 'Invalid Request',
+ 'name': 'InvalidRequestError',
+ },
+ 'id': None,
+ 'jsonrpc': '2.0',
+ }
+ assert rv.status_code == 400
+
rv = client.post(
'/api',
data=json.dumps({'id': 1, 'jsonrpc': '2.0', 'method': 'app.index'}),
diff --git a/tests/test_apps/app/__init__.py b/tests/test_apps/app/__init__.py
index 0dde38ae..9ff8d046 100644
--- a/tests/test_apps/app/__init__.py
+++ b/tests/test_apps/app/__init__.py
@@ -62,6 +62,9 @@ def echo(self, string: str, _some: t.Any = None) -> str:
def notify(self, _string: str = None) -> None:
pass
+ def not_allow_notify(self, _string: str = None) -> str:
+ return 'Now allow notify'
+
def fails(self, n: int) -> int:
if n % 2 == 0:
return n
@@ -100,6 +103,11 @@ def echo(string: str, _some: t.Any = None) -> str:
def notify(_string: str = None) -> None:
pass
+ # pylint: disable=W0612
+ @jsonrpc.method('jsonrpc.not_allow_notify', notification=False)
+ def not_allow_notify(_string: str = None) -> str:
+ return 'Not allow notify'
+
# pylint: disable=W0612
@jsonrpc.method('jsonrpc.fails')
def fails(n: int) -> int:
@@ -146,6 +154,7 @@ def return_status_code_and_headers(s: str) -> t.Tuple[str, int, t.Dict[str, t.An
jsonrpc.register(class_app.hello)
jsonrpc.register(class_app.echo)
jsonrpc.register(class_app.notify)
+ jsonrpc.register(class_app.not_allow_notify, notification=False)
jsonrpc.register(class_app.fails)
return flask_app
diff --git a/tests/test_apps/async_app/__init__.py b/tests/test_apps/async_app/__init__.py
index 79e9af6f..6b6ceff2 100644
--- a/tests/test_apps/async_app/__init__.py
+++ b/tests/test_apps/async_app/__init__.py
@@ -62,6 +62,9 @@ def echo(self, string: str, _some: t.Any = None) -> str:
def notify(self, _string: str = None) -> None:
pass
+ def not_allow_notify(self, _string: str = None) -> str:
+ return 'Now allow notify'
+
def fails(self, n: int) -> int:
if n % 2 == 0:
return n
@@ -102,6 +105,12 @@ async def echo(string: str, _some: t.Any = None) -> str:
async def notify(_string: str = None) -> None:
await asyncio.sleep(0)
+ # pylint: disable=W0612
+ @jsonrpc.method('jsonrpc.not_allow_notify', notification=False)
+ async def not_allow_notify(_string: str = None) -> str:
+ await asyncio.sleep(0)
+ return 'Not allow notify'
+
# pylint: disable=W0612
@jsonrpc.method('jsonrpc.fails')
async def fails(n: int) -> int:
@@ -155,6 +164,7 @@ async def return_status_code_and_headers(s: str) -> t.Tuple[str, int, t.Dict[str
jsonrpc.register(class_app.hello)
jsonrpc.register(class_app.echo)
jsonrpc.register(class_app.notify)
+ jsonrpc.register(class_app.not_allow_notify, notification=False)
jsonrpc.register(class_app.fails)
return flask_app
diff --git a/tests/test_async_app.py b/tests/test_async_app.py
index 4df6e447..4ac19e4a 100644
--- a/tests/test_async_app.py
+++ b/tests/test_async_app.py
@@ -74,6 +74,11 @@ async def fn3(s: str) -> str:
await asyncio.sleep(0)
return f'Foo {s}'
+ # pylint: disable=W0612
+ @jsonrpc.method('app.fn4', notification=False)
+ def fn4(s: str) -> str:
+ return f'Goo {s}'
+
jsonrpc.register(fn3, name='app.fn3')
with app.test_client() as client:
@@ -97,6 +102,26 @@ async def fn3(s: str) -> str:
assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo :)'}
assert rv.status_code == 200
+ rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']})
+ assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Goo :)'}
+ assert rv.status_code == 200
+
+ rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)']})
+ assert rv.json == {
+ 'error': {
+ 'code': -32600,
+ 'data': {
+ 'message': "The method 'app.fn4' doesn't allow Notification Request "
+ "object (without an 'id' member)"
+ },
+ 'message': 'Invalid Request',
+ 'name': 'InvalidRequestError',
+ },
+ 'id': None,
+ 'jsonrpc': '2.0',
+ }
+ assert rv.status_code == 400
+
rv = client.post(
'/api',
data=json.dumps({'id': 1, 'jsonrpc': '2.0', 'method': 'app.index'}),
diff --git a/tests/test_async_client.py b/tests/test_async_client.py
index ad87aec3..b88e212f 100644
--- a/tests/test_async_client.py
+++ b/tests/test_async_client.py
@@ -285,6 +285,30 @@ def test_app_notify(async_client):
assert rv.status_code == 204
+def test_app_not_allow_notify(client):
+ rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify'})
+ assert rv.json == {
+ 'error': {
+ 'code': -32600,
+ 'data': {
+ 'message': "The method 'jsonrpc.not_allow_notify' doesn't allow Notification Request "
+ "object (without an 'id' member)"
+ },
+ 'message': 'Invalid Request',
+ 'name': 'InvalidRequestError',
+ },
+ 'id': None,
+ 'jsonrpc': '2.0',
+ }
+ assert rv.status_code == 400
+
+ rv = client.post(
+ '/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify', 'params': ['Some string']}
+ )
+ assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Not allow notify'}
+ assert rv.status_code == 200
+
+
def test_app_fails(async_client):
rv = async_client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.fails', 'params': [2]})
assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 2}
@@ -466,30 +490,42 @@ def test_app_system_describe(async_client):
assert rv.json['result']['procs'] == [
{
'name': 'jsonrpc.greeting',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'jsonrpc.echo',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'jsonrpc.notify',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': '_string', 'type': 'String'}],
'return': {'type': 'Null'},
'summary': None,
},
+ {
+ 'name': 'jsonrpc.not_allow_notify',
+ 'options': {'notification': False, 'validate': True},
+ 'params': [{'name': '_string', 'type': 'String'}],
+ 'return': {'type': 'String'},
+ 'summary': None,
+ },
{
'name': 'jsonrpc.fails',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'n', 'type': 'Number'}],
'return': {'type': 'Number'},
'summary': None,
},
{
'name': 'jsonrpc.strangeEcho',
+ 'options': {'notification': True, 'validate': True},
'params': [
{'name': 'string', 'type': 'String'},
{'name': 'omg', 'type': 'Object'},
@@ -502,65 +538,88 @@ def test_app_system_describe(async_client):
},
{
'name': 'jsonrpc.sum',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}],
'return': {'type': 'Number'},
'summary': None,
},
{
'name': 'jsonrpc.decorators',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'string', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'jsonrpc.returnStatusCode',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'Array'},
'summary': None,
},
{
'name': 'jsonrpc.returnHeaders',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'Array'},
'summary': None,
},
{
'name': 'jsonrpc.returnStatusCodeAndHeaders',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'Array'},
'summary': None,
},
{
'name': 'classapp.index',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'greeting',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'hello',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'echo',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'notify',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': '_string', 'type': 'String'}],
'return': {'type': 'Null'},
'summary': None,
},
- {'name': 'fails', 'params': [{'name': 'n', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None},
+ {
+ 'name': 'not_allow_notify',
+ 'options': {'notification': False, 'validate': True},
+ 'params': [{'name': '_string', 'type': 'String'}],
+ 'return': {'type': 'String'},
+ 'summary': None,
+ },
+ {
+ 'name': 'fails',
+ 'options': {'notification': True, 'validate': True},
+ 'params': [{'name': 'n', 'type': 'Number'}],
+ 'return': {'type': 'Number'},
+ 'summary': None,
+ },
]
assert rv.status_code == 200
diff --git a/tests/test_client.py b/tests/test_client.py
index 1c041ef6..459c48b4 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -275,6 +275,30 @@ def test_app_notify(client):
assert rv.status_code == 204
+def test_app_not_allow_notify(client):
+ rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify'})
+ assert rv.json == {
+ 'error': {
+ 'code': -32600,
+ 'data': {
+ 'message': "The method 'jsonrpc.not_allow_notify' doesn't allow Notification Request "
+ "object (without an 'id' member)"
+ },
+ 'message': 'Invalid Request',
+ 'name': 'InvalidRequestError',
+ },
+ 'id': None,
+ 'jsonrpc': '2.0',
+ }
+ assert rv.status_code == 400
+
+ rv = client.post(
+ '/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.not_allow_notify', 'params': ['Some string']}
+ )
+ assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Not allow notify'}
+ assert rv.status_code == 200
+
+
def test_app_fails(client):
rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.fails', 'params': [2]})
assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 2}
@@ -452,30 +476,42 @@ def test_app_system_describe(client):
assert rv.json['result']['procs'] == [
{
'name': 'jsonrpc.greeting',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'jsonrpc.echo',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'jsonrpc.notify',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': '_string', 'type': 'String'}],
'return': {'type': 'Null'},
'summary': None,
},
+ {
+ 'name': 'jsonrpc.not_allow_notify',
+ 'options': {'notification': False, 'validate': True},
+ 'params': [{'name': '_string', 'type': 'String'}],
+ 'return': {'type': 'String'},
+ 'summary': None,
+ },
{
'name': 'jsonrpc.fails',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'n', 'type': 'Number'}],
'return': {'type': 'Number'},
'summary': None,
},
{
'name': 'jsonrpc.strangeEcho',
+ 'options': {'notification': True, 'validate': True},
'params': [
{'name': 'string', 'type': 'String'},
{'name': 'omg', 'type': 'Object'},
@@ -488,65 +524,88 @@ def test_app_system_describe(client):
},
{
'name': 'jsonrpc.sum',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}],
'return': {'type': 'Number'},
'summary': None,
},
{
'name': 'jsonrpc.decorators',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'string', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'jsonrpc.returnStatusCode',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'Array'},
'summary': None,
},
{
'name': 'jsonrpc.returnHeaders',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'Array'},
'summary': None,
},
{
'name': 'jsonrpc.returnStatusCodeAndHeaders',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 's', 'type': 'String'}],
'return': {'type': 'Array'},
'summary': None,
},
{
'name': 'classapp.index',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'greeting',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'hello',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'name', 'type': 'String'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'echo',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': 'string', 'type': 'String'}, {'name': '_some', 'type': 'Object'}],
'return': {'type': 'String'},
'summary': None,
},
{
'name': 'notify',
+ 'options': {'notification': True, 'validate': True},
'params': [{'name': '_string', 'type': 'String'}],
'return': {'type': 'Null'},
'summary': None,
},
- {'name': 'fails', 'params': [{'name': 'n', 'type': 'Number'}], 'return': {'type': 'Number'}, 'summary': None},
+ {
+ 'name': 'not_allow_notify',
+ 'options': {'notification': False, 'validate': True},
+ 'params': [{'name': '_string', 'type': 'String'}],
+ 'return': {'type': 'String'},
+ 'summary': None,
+ },
+ {
+ 'name': 'fails',
+ 'options': {'notification': True, 'validate': True},
+ 'params': [{'name': 'n', 'type': 'Number'}],
+ 'return': {'type': 'Number'},
+ 'summary': None,
+ },
]
assert rv.status_code == 200