From b750ec055f22fa882eced70095b8399202b0cde3 Mon Sep 17 00:00:00 2001 From: pavan-maddula Date: Mon, 7 Jan 2019 16:08:09 +0530 Subject: [PATCH 1/4] Error codes support --- kwikapi/api.py | 22 ++++++++--- kwikapi/auth.py | 3 +- kwikapi/client.py | 9 +++-- kwikapi/exception.py | 93 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 14 deletions(-) diff --git a/kwikapi/api.py b/kwikapi/api.py index 6492214..21ea2c8 100644 --- a/kwikapi/api.py +++ b/kwikapi/api.py @@ -20,6 +20,7 @@ from .exception import DuplicateAPIFunction, UnknownAPIFunction from .exception import ProtocolAlreadyExists, UnknownProtocol from .exception import UnsupportedType, TypeNotSpecified +from .exception import KeywordArgumentError from .utils import get_loggable_params @@ -343,6 +344,7 @@ def get_api_fn(self, fn_name, version, namespace): class BaseRequestHandler(object): PROTOCOLS = PROTOCOLS DEFAULT_PROTOCOL = DEFAULT_PROTOCOL + DEFAULT_ERROR_CODE = 50000 def __init__(self, api, default_version=None, default_protocol=DEFAULT_PROTOCOL, @@ -455,8 +457,11 @@ def _find_request_protocol(self, request): return self.PROTOCOLS[protocol] def _handle_exception(self, req, e): - message = e.message if hasattr(e, 'message') else str(e) - message = '[(%s) %s: %s]' % (self.api._id, e.__class__.__name__, message) + message_value = e.message if hasattr(e, 'message') else str(e) + code_value = e.code if hasattr(e, 'code') else self.DEFAULT_ERROR_CODE + error_value = '[(%s) %s]' % (self.api._id, e.__class__.__name__) + success_value = False + message = dict(message=message_value, code=code_value, error=error_value, success=success_value) _log = req.log if hasattr(req, 'log') else self.log _log.exception('handle_request_error', message=message, @@ -470,7 +475,7 @@ def _wrap_stream(self, req, res): yield dict(success=True, result=r) except Exception as e: m = self._handle_exception(req, e) - yield dict(success=False, message=m) + yield m def handle_request(self, request): if self.api._auth: @@ -486,7 +491,14 @@ def handle_request(self, request): # invoke the API function tcompute = time.time() - result = request.fn(**request.fn_params) + try: + result = request.fn(**request.fn_params) + except TypeError as e: + if 'got an unexpected keyword argument' in str(e): + raise KeywordArgumentError(e.args[0]) + else: + raise e + tcompute = time.time() - tcompute response.headers[TIMING_HEADER] = str(tcompute) @@ -508,7 +520,7 @@ def handle_request(self, request): except Exception as e: m = self._handle_exception(request, e) - response.write(dict(success=False, message=m), protocol) + response.write(m, protocol) response.flush() response.close() diff --git a/kwikapi/auth.py b/kwikapi/auth.py index b3f5fa7..600d16a 100644 --- a/kwikapi/auth.py +++ b/kwikapi/auth.py @@ -5,6 +5,7 @@ from deeputil import xcode, AttrDict from .api import Request +from .exception import AuthenticationError class BaseServerAuthenticator: '''Helps in authenticating a request on the server''' @@ -16,7 +17,7 @@ def _read_auth(self, req): auth = req.headers.get('Authorization') _type, info = auth.split(' ', 1) if _type.lower() != self.TYPE: - raise Exception('Invalid auth type: %s' % _type) # FIXME: raise exc hierarchy + raise AuthenticationError(_type) auth = AuthInfo(type=self.TYPE) auth.header_info = info diff --git a/kwikapi/client.py b/kwikapi/client.py index 48e589b..ad19f8a 100644 --- a/kwikapi/client.py +++ b/kwikapi/client.py @@ -6,7 +6,7 @@ from deeputil import Dummy, ExpiringCache from .protocols import PROTOCOLS -from .exception import APICallFailed +from .exception import NonKeywordArgumentsError from .api import PROTOCOL_HEADER, REQUEST_ID_HEADER from .utils import get_loggable_params @@ -47,7 +47,6 @@ class Client: def __init__(self, url, version=None, protocol=DEFAULT_PROTOCOL, path=None, request='', timeout=None, dnscache=None, headers=None, auth=None, stream=False, log=DUMMY_LOG): - headers = headers or {} self._url = url @@ -127,7 +126,8 @@ def _deserialize_response(data, protocol): def _extract_response(r): success = r['success'] if not success: - raise Exception(r['message']) # FIXME: raise proper exc + r.pop('success') + raise Exception(r) # FIXME: raise proper exc else: r = r['result'] @@ -145,7 +145,8 @@ def _serialize_params(params, protocol): return data def __call__(self, *args, **kwargs): - assert(not args) # FIXME: raise appropriate exception + if args: + raise NonKeywordArgumentsError(args) if self._path: # FIXME: support streaming in both directions diff --git a/kwikapi/exception.py b/kwikapi/exception.py index c5da5c1..e5c409c 100644 --- a/kwikapi/exception.py +++ b/kwikapi/exception.py @@ -8,6 +8,11 @@ class BaseException(Exception): def message(self): pass + @abc.abstractmethod + def code(self): + pass + + class BaseServerException(BaseException): pass @@ -23,6 +28,11 @@ def __init__(self, version, api_fn): def message(self): return '"%s" API function already exists in the version "%s"' % (self.api_fn, self.version) + @property + def code(self): + return 50001 + + class UnknownAPIFunction(BaseException): def __init__(self, api_fn_name): self.fn_name = api_fn_name @@ -31,6 +41,11 @@ def __init__(self, api_fn_name): def message(self): return 'Unknown API Function: "%s"' % self.fn_name + @property + def code(self): + return 50002 + + class ProtocolAlreadyExists(BaseServerException): def __init__(self, proto): self.proto = proto @@ -39,6 +54,11 @@ def __init__(self, proto): def message(self): return '"%s" is already exists' % self.proto + @property + def code(self): + return 50003 + + class UnknownProtocol(BaseException): def __init__(self, proto): self.proto = proto @@ -47,6 +67,11 @@ def __init__(self, proto): def message(self): return '"%s" protocol is not exist to make it default' % self.proto + @property + def code(self): + return 50004 + + class UnknownVersion(BaseException): def __init__(self, version): self.version = version @@ -55,6 +80,11 @@ def __init__(self, version): def message(self): return '"%s" There are no methods associated with this version' % self.version + @property + def code(self): + return 50005 + + class UnsupportedType(BaseServerException): def __init__(self, _type): self._type = _type @@ -63,6 +93,11 @@ def __init__(self, _type): def message(self): return '"%s" type is not supported' % self._type + @property + def code(self): + return 50006 + + class TypeNotSpecified(BaseServerException): def __init__(self, arg): self.arg = arg @@ -71,6 +106,12 @@ def __init__(self, arg): def message(self): return 'Please specify type for the argument "%s"' % (self.arg) + @property + def code(self): + return 50007 + + + class UnknownVersionOrNamespace(BaseException): def __init__(self, arg): self.arg = arg @@ -79,6 +120,11 @@ def __init__(self, arg): def message(self): return 'No methods associated with this version "%s" or namespace "%s".' % (self.arg[0], self.arg[1]) + @property + def code(self): + return 50008 + + class StreamingNotSupported(BaseException): def __init__(self, proto): self.proto = proto @@ -87,10 +133,49 @@ def __init__(self, proto): def message(self): return 'Streaming not supported for "%s" protocol' % self.proto -class APICallFailed(BaseClientException): - def __init__(self, code): - self.code = code + @property + def code(self): + return 50009 + + +class KeywordArgumentError(BaseException): + def __init__(self, error_message): + self.error_message = error_message + + @property + def message(self): + return self.error_message + + @property + def code(self): + return 50010 + +class AuthenticationError(BaseException): + def __init__(self, error_type): + self._type = error_type + + @property + def message(self): + return 'Invalid auth type: %s' % self._type + + @property + def code(self): + return 50011 + + +class NonKeywordArgumentsError(BaseException): + def __init__(self, non_keyword_args): + self.non_keyword_args = ','.join(map(str, non_keyword_args)) + self.error_value = '[() %s]' % (self.__class__.__name__) + response_message = dict(message=self.message,code=self.code, + error=self.error_value) + super(NonKeywordArgumentsError, self).__init__(response_message) + @property def message(self): - return 'HTTP Error code: %d' % self.code + return 'Found non keyword arguments: %s' % self.non_keyword_args + + @property + def code(self): + return 50012 From e0e3df6df207ed7708074b39c2df144af7ab7494 Mon Sep 17 00:00:00 2001 From: pavan-maddula Date: Tue, 8 Jan 2019 16:26:51 +0530 Subject: [PATCH 2/4] Update api.py --- kwikapi/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kwikapi/api.py b/kwikapi/api.py index 21ea2c8..d5bd079 100644 --- a/kwikapi/api.py +++ b/kwikapi/api.py @@ -474,8 +474,7 @@ def _wrap_stream(self, req, res): for r in res: yield dict(success=True, result=r) except Exception as e: - m = self._handle_exception(req, e) - yield m + yield self._handle_exception(req, e) def handle_request(self, request): if self.api._auth: From ffa362ddc84a63ff320d4b9c2481dead37a15ba1 Mon Sep 17 00:00:00 2001 From: pavan-maddula Date: Fri, 18 Jan 2019 11:32:23 +0530 Subject: [PATCH 3/4] update README with error codes and error messages --- .travis.yml | 4 +-- README.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++- setup.py | 2 +- 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5eef1db..7d8e988 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,8 @@ deploy: - LICENSE - kwikapi/api.py - kwikapi/__init__.py - name: kwikapi-0.4.7 - tag_name: 0.4.7 + name: kwikapi-0.4.9 + tag_name: 0.4.9 on: repo: deep-compute/kwikapi - provider: pypi diff --git a/README.md b/README.md index 798f3ba..d36f826 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ True - Bulk request handling - KwikAPI Client - Authentication +- Custom error codes and messages ### Versioning support Versioning support will be used if user wants different versions of functionality with slightly changed behaviour. @@ -643,7 +644,7 @@ api.register(Calc(), 'v1') def make_app(): return tornado.web.Application([ (r'^/api/.*', RequestHandler, dict(api=api)), - ]) + ]) if __name__ == "__main__": app = make_app() app.listen(8818) @@ -722,6 +723,95 @@ auth = b'Bearer %s' % b'key' headers['Authorization'] = auth ``` +### Custom error codes and messages +KwikAPI supports error `codes` and `messages`. + +A global map of error messages and error codes are maintained across the KwikAPI. Every error code specifies a unique error message that is possible. + +**KwikAPI's default error code:** + +| Error | Code | +| ---- | ---- | +| Unknown / Internal Exception | 50000 | +| Duplicate API function | 50001 | +| Unknown API function | 50002 | +| Protocol already exists | 50003 | +| Unknown protocol | 50004 | +| Unknown version | 50005 | +| Unsupported type | 50006 | +| Type not specified | 50007 | +| Unknown version or namespace | 50008 | +| Streaming not supported | 50009 | +| Keyword argument error | 50010 | +| Authentication error | 50011 | +| Non-keyword arguments error | 50012 | + +Exceptions raised by API's return default error if not handled which is `50000` by default. + +#### Examples +- Response with an error code and message: + > URL:`https://www.example.com/addd?a=10&b=20` + + ```json + { + "message": "Unknown API Function: \"addd\"", + "code": 50002, + "error": "[(www.example.com) UnknownAPIFunction]", + "success": false + } + ``` + + To return custom error messages and code, developer must raise an exception object with attributes `message` and `code` in it. + +- Raising custom error message and error code + ```python + import tornado.web + import tornado.ioloop + + from kwikapi import API + from kwikapi.tornado import RequestHandler + + # custom exception + class CalcError(Exception): + def __init__(self, message="Input error", code=1101): + self.message = message + self.code = code + + # Core logic that you want to expose as a service + class Calc(object): + def divide(self, a: int, b: int) -> int: + try: + return a / b + except: + raise CalcError(message="b can't be zero") + + # Register BaseCalc with KwikAPI + api = API() + api.register(Calc(), 'v1') + + # Passing RequestHandler to the KwikAPI + def make_app(): + return tornado.web.Application([ + (r'^/api/.*', RequestHandler, dict(api=api)), + ]) + + # Starting the application + if __name__ == "__main__": + app = make_app() + app.listen(8888) + tornado.ioloop.IOLoop.current().start() + ``` + Response: + > URL:`https://www.example.com/divide?a=10&b=0` + + ```json + { + "success": false, + "message": "b can't be zero", + "code": 1101, + "error": "[(www.example.com) CalcError]" + } + ``` ## Run test cases ```bash $ python3 -m doctest -v README.md diff --git a/setup.py b/setup.py index 0e73553..82e2cd2 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.4.7' +version = '0.4.9' setup( name="kwikapi", version=version, From cd4834672139a18d25876ca56125057474332baa Mon Sep 17 00:00:00 2001 From: pavan-maddula Date: Tue, 22 Jan 2019 19:45:22 +0530 Subject: [PATCH 4/4] Update version --- .travis.yml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7d8e988..5de8583 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,8 @@ deploy: - LICENSE - kwikapi/api.py - kwikapi/__init__.py - name: kwikapi-0.4.9 - tag_name: 0.4.9 + name: kwikapi-0.5.0 + tag_name: 0.5.0 on: repo: deep-compute/kwikapi - provider: pypi diff --git a/setup.py b/setup.py index 82e2cd2..58cf648 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.4.9' +version = '0.5.0' setup( name="kwikapi", version=version,