api: add get_method API call.
- in addition use it's mechanics to propose users other methods if the calling method is not found.
This commit is contained in:
parent
a2de6540c6
commit
6568814f98
4 changed files with 171 additions and 5 deletions
|
|
@ -22,6 +22,7 @@ import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import types
|
import types
|
||||||
|
import fnmatch
|
||||||
|
|
||||||
import decorator
|
import decorator
|
||||||
import venusian
|
import venusian
|
||||||
|
|
@ -47,6 +48,18 @@ DEFAULT_RENDERER = 'jsonrpc_renderer'
|
||||||
DEFAULT_URL = '/_admin/apiv2'
|
DEFAULT_URL = '/_admin/apiv2'
|
||||||
|
|
||||||
|
|
||||||
|
def find_methods(jsonrpc_methods, pattern):
|
||||||
|
matches = OrderedDict()
|
||||||
|
if not isinstance(pattern, (list, tuple)):
|
||||||
|
pattern = [pattern]
|
||||||
|
|
||||||
|
for single_pattern in pattern:
|
||||||
|
for method_name, method in jsonrpc_methods.items():
|
||||||
|
if fnmatch.fnmatch(method_name, single_pattern):
|
||||||
|
matches[method_name] = method
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
class ExtJsonRenderer(object):
|
class ExtJsonRenderer(object):
|
||||||
"""
|
"""
|
||||||
Custom renderer that mkaes use of our ext_json lib
|
Custom renderer that mkaes use of our ext_json lib
|
||||||
|
|
@ -143,7 +156,19 @@ def exception_view(exc, request):
|
||||||
log.debug('json-rpc method `%s` not found in list of '
|
log.debug('json-rpc method `%s` not found in list of '
|
||||||
'api calls: %s, rpc_id:%s',
|
'api calls: %s, rpc_id:%s',
|
||||||
method, request.registry.jsonrpc_methods.keys(), rpc_id)
|
method, request.registry.jsonrpc_methods.keys(), rpc_id)
|
||||||
fault_message = "No such method: {}".format(method)
|
|
||||||
|
similar = 'none'
|
||||||
|
try:
|
||||||
|
similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
|
||||||
|
similar_found = find_methods(
|
||||||
|
request.registry.jsonrpc_methods, similar_paterns)
|
||||||
|
similar = ', '.join(similar_found.keys()) or similar
|
||||||
|
except Exception:
|
||||||
|
# make the whole above block safe
|
||||||
|
pass
|
||||||
|
|
||||||
|
fault_message = "No such method: {}. Similar methods: {}".format(
|
||||||
|
method, similar)
|
||||||
|
|
||||||
return jsonrpc_error(request, fault_message, rpc_id)
|
return jsonrpc_error(request, fault_message, rpc_id)
|
||||||
|
|
||||||
|
|
@ -348,9 +373,10 @@ class RoutePredicate(object):
|
||||||
class NotFoundPredicate(object):
|
class NotFoundPredicate(object):
|
||||||
def __init__(self, val, config):
|
def __init__(self, val, config):
|
||||||
self.val = val
|
self.val = val
|
||||||
|
self.methods = config.registry.jsonrpc_methods
|
||||||
|
|
||||||
def text(self):
|
def text(self):
|
||||||
return 'jsonrpc method not found = %s' % self.val
|
return 'jsonrpc method not found = {}.'.format(self.val)
|
||||||
|
|
||||||
phash = text
|
phash = text
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,13 @@ class TestApi(object):
|
||||||
def test_api_non_existing_method(self, request):
|
def test_api_non_existing_method(self, request):
|
||||||
id_, params = build_data(self.apikey, 'not_existing', args='xx')
|
id_, params = build_data(self.apikey, 'not_existing', args='xx')
|
||||||
response = api_call(self.app, params)
|
response = api_call(self.app, params)
|
||||||
expected = 'No such method: not_existing'
|
expected = 'No such method: not_existing. Similar methods: none'
|
||||||
|
assert_error(id_, expected, given=response.body)
|
||||||
|
|
||||||
|
def test_api_non_existing_method_have_similar(self, request):
|
||||||
|
id_, params = build_data(self.apikey, 'comment', args='xx')
|
||||||
|
response = api_call(self.app, params)
|
||||||
|
expected = 'No such method: comment. Similar methods: changeset_comment, comment_pull_request, comment_commit'
|
||||||
assert_error(id_, expected, given=response.body)
|
assert_error(id_, expected, given=response.body)
|
||||||
|
|
||||||
def test_api_disabled_user(self, request):
|
def test_api_disabled_user(self, request):
|
||||||
|
|
|
||||||
58
rhodecode/api/tests/test_get_method.py
Normal file
58
rhodecode/api/tests/test_get_method.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (C) 2010-2017 RhodeCode GmbH
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License, version 3
|
||||||
|
# (only), as published by the Free Software Foundation.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# This program is dual-licensed. If you wish to learn more about the
|
||||||
|
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||||
|
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||||
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from rhodecode.api.tests.utils import build_data, api_call, assert_ok
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("testuser_api", "app")
|
||||||
|
class TestGetMethod(object):
|
||||||
|
def test_get_methods_no_matches(self):
|
||||||
|
id_, params = build_data(self.apikey, 'get_method', pattern='hello')
|
||||||
|
response = api_call(self.app, params)
|
||||||
|
|
||||||
|
expected = []
|
||||||
|
assert_ok(id_, expected, given=response.body)
|
||||||
|
|
||||||
|
def test_get_methods(self):
|
||||||
|
id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
|
||||||
|
response = api_call(self.app, params)
|
||||||
|
|
||||||
|
expected = ['changeset_comment', 'comment_pull_request',
|
||||||
|
'comment_commit']
|
||||||
|
assert_ok(id_, expected, given=response.body)
|
||||||
|
|
||||||
|
def test_get_methods_on_single_match(self):
|
||||||
|
id_, params = build_data(self.apikey, 'get_method', pattern='*comment_commit*')
|
||||||
|
response = api_call(self.app, params)
|
||||||
|
|
||||||
|
expected = ['comment_commit',
|
||||||
|
{'apiuser': '<RequiredType>',
|
||||||
|
'comment_type': "<Optional:u'note'>",
|
||||||
|
'commit_id': '<RequiredType>',
|
||||||
|
'message': '<RequiredType>',
|
||||||
|
'repoid': '<RequiredType>',
|
||||||
|
'request': '<RequiredType>',
|
||||||
|
'resolves_comment_id': '<Optional:None>',
|
||||||
|
'status': '<Optional:None>',
|
||||||
|
'userid': '<Optional:<OptionalAttr:apiuser>>'}]
|
||||||
|
assert_ok(id_, expected, given=response.body)
|
||||||
|
|
@ -18,10 +18,12 @@
|
||||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||||
|
|
||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
import itertools
|
||||||
|
|
||||||
from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
|
from rhodecode.api import (
|
||||||
|
jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
|
||||||
|
|
||||||
from rhodecode.api.utils import (
|
from rhodecode.api.utils import (
|
||||||
Optional, OAttr, has_superadmin_permission, get_user_or_error)
|
Optional, OAttr, has_superadmin_permission, get_user_or_error)
|
||||||
|
|
@ -243,3 +245,77 @@ def cleanup_sessions(request, apiuser, older_then=Optional(60)):
|
||||||
raise JSONRPCError(
|
raise JSONRPCError(
|
||||||
'Error occurred during session cleanup'
|
'Error occurred during session cleanup'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@jsonrpc_method()
|
||||||
|
def get_method(request, apiuser, pattern=Optional('*')):
|
||||||
|
"""
|
||||||
|
Returns list of all available API methods. By default match pattern
|
||||||
|
os "*" but any other pattern can be specified. eg *comment* will return
|
||||||
|
all methods with comment inside them. If just single method is matched
|
||||||
|
returned data will also include method specification
|
||||||
|
|
||||||
|
This command can only be run using an |authtoken| with admin rights to
|
||||||
|
the specified repository.
|
||||||
|
|
||||||
|
This command takes the following options:
|
||||||
|
|
||||||
|
:param apiuser: This is filled automatically from the |authtoken|.
|
||||||
|
:type apiuser: AuthUser
|
||||||
|
:param pattern: pattern to match method names against
|
||||||
|
:type older_then: Optional("*")
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
id : <id_given_in_input>
|
||||||
|
"result": [
|
||||||
|
"changeset_comment",
|
||||||
|
"comment_pull_request",
|
||||||
|
"comment_commit"
|
||||||
|
]
|
||||||
|
error : null
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
id : <id_given_in_input>
|
||||||
|
"result": [
|
||||||
|
"comment_commit",
|
||||||
|
{
|
||||||
|
"apiuser": "<RequiredType>",
|
||||||
|
"comment_type": "<Optional:u'note'>",
|
||||||
|
"commit_id": "<RequiredType>",
|
||||||
|
"message": "<RequiredType>",
|
||||||
|
"repoid": "<RequiredType>",
|
||||||
|
"request": "<RequiredType>",
|
||||||
|
"resolves_comment_id": "<Optional:None>",
|
||||||
|
"status": "<Optional:None>",
|
||||||
|
"userid": "<Optional:<OptionalAttr:apiuser>>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
error : null
|
||||||
|
"""
|
||||||
|
if not has_superadmin_permission(apiuser):
|
||||||
|
raise JSONRPCForbidden()
|
||||||
|
|
||||||
|
pattern = Optional.extract(pattern)
|
||||||
|
|
||||||
|
matches = find_methods(request.registry.jsonrpc_methods, pattern)
|
||||||
|
|
||||||
|
args_desc = []
|
||||||
|
if len(matches) == 1:
|
||||||
|
func = matches[matches.keys()[0]]
|
||||||
|
|
||||||
|
argspec = inspect.getargspec(func)
|
||||||
|
arglist = argspec[0]
|
||||||
|
defaults = map(repr, argspec[3] or [])
|
||||||
|
|
||||||
|
default_empty = '<RequiredType>'
|
||||||
|
|
||||||
|
# kw arguments required by this method
|
||||||
|
func_kwargs = dict(itertools.izip_longest(
|
||||||
|
reversed(arglist), reversed(defaults), fillvalue=default_empty))
|
||||||
|
args_desc.append(func_kwargs)
|
||||||
|
|
||||||
|
return matches.keys() + args_desc
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue