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:
Marcin Kuzminski 2017-02-10 16:24:57 +01:00
parent a2de6540c6
commit 6568814f98
4 changed files with 171 additions and 5 deletions

View file

@ -22,6 +22,7 @@ import inspect
import itertools
import logging
import types
import fnmatch
import decorator
import venusian
@ -47,6 +48,18 @@ DEFAULT_RENDERER = 'jsonrpc_renderer'
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):
"""
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 '
'api calls: %s, rpc_id:%s',
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)
@ -348,9 +373,10 @@ class RoutePredicate(object):
class NotFoundPredicate(object):
def __init__(self, val, config):
self.val = val
self.methods = config.registry.jsonrpc_methods
def text(self):
return 'jsonrpc method not found = %s' % self.val
return 'jsonrpc method not found = {}.'.format(self.val)
phash = text

View file

@ -80,7 +80,13 @@ class TestApi(object):
def test_api_non_existing_method(self, request):
id_, params = build_data(self.apikey, 'not_existing', args='xx')
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)
def test_api_disabled_user(self, request):

View 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)

View file

@ -18,10 +18,12 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import inspect
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 (
Optional, OAttr, has_superadmin_permission, get_user_or_error)
@ -243,3 +245,77 @@ def cleanup_sessions(request, apiuser, older_then=Optional(60)):
raise JSONRPCError(
'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