From 518885a39f8a141c311c1f18cb59efea5f583c88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 18 Oct 2019 00:26:10 +0200 Subject: [PATCH] added docstrings --- pype/services/rest_api/base_class.py | 53 +++++++++++++++++++ pype/services/rest_api/lib/exceptions.py | 1 + pype/services/rest_api/lib/factory.py | 67 +++++++++++++++++++++++- pype/services/rest_api/lib/handler.py | 35 +++++++++++-- pype/services/rest_api/lib/lib.py | 38 ++++++++++++++ pype/services/rest_api/rest_api.py | 66 ++++++++++++++++++++++- 6 files changed, 254 insertions(+), 6 deletions(-) diff --git a/pype/services/rest_api/base_class.py b/pype/services/rest_api/base_class.py index 8d4f7cbaa5..5355c5886c 100644 --- a/pype/services/rest_api/base_class.py +++ b/pype/services/rest_api/base_class.py @@ -8,6 +8,32 @@ from .lib import ( def route(path, url_prefix="", methods=[]): + """Decorator that register callback and all its attributes. + Callback is registered to Singleton RestApiFactory. + + :param path: Specify url path when callback should be triggered. + :type path: str + :param url_prefix: Specify prefix of path, defaults to "/". + :type url_prefix: str, list, optional + :param methods: Specify request method (GET, POST, PUT, UPDATE, DELETE) when callback will be triggered, defaults to ["GET"] + :type methods: list, str, optional + + `path` may include dynamic keys that will be stored to object which can + be obtained in callback. + Example: + - registered path: "/projects/" + - url request path: "/projects/S001_test_project" + In this case will be callback triggered and in accessible data will be + stored {"project_name": "S001_test_project"}. + + `url_prefix` is optional but it is better to specify for easier filtering + of requests. + Example: + - url_prefix: `"/avalon"` or `["avalon"]` + - path: `"/projects"` + In this case request path must be "/avalon/projects" to trigger registered + callback. + """ def decorator(callback): RestApiFactory.register_route(path, callback, url_prefix, methods) callback.restapi = True @@ -16,10 +42,29 @@ def route(path, url_prefix="", methods=[]): def register_statics(url_prefix, dir_path): + """Decorator that register callback and all its attributes. + Callback is registered to Singleton RestApiFactory. + + :param url_prefix: Specify prefix of path, defaults to "/". (Example: "/resources") + :type url_prefix: str + :param dir_path: Path to file folder where statics are located. + :type dir_path: str + """ RestApiFactory.register_statics((url_prefix, dir_path)) def abort(status_code=HTTPStatus.NOT_FOUND, message=None): + """Should be used to stop registered callback + `abort` raise AbortException that is handled with request Handler which + returns entered status and may send optional message in body. + + :param status_code: Status that will be send in reply of request, defaults to 404 + :type status_code: int + :param message: Message to send in body, default messages are based on statuc_code in Handler, defaults to None + :type message: str, optional + ... + :raises AbortException: This exception is handled in Handler to know about launched `abort` + """ items = [] items.append(str(status_code)) if not message: @@ -31,6 +76,14 @@ def abort(status_code=HTTPStatus.NOT_FOUND, message=None): class RestApi: + """Base class for RestApi classes. + + Use this class is required when it is necessary to have class for handling + requests and want to use decorators for registering callbacks. + + It is possible to use decorators in another class only when object, of class + where decorators are, is registered to RestApiFactory. + """ def route(path, url_prefix="", methods=[]): return route(path, url_prefix, methods) diff --git a/pype/services/rest_api/lib/exceptions.py b/pype/services/rest_api/lib/exceptions.py index 849b522ddd..118fa4ffc8 100644 --- a/pype/services/rest_api/lib/exceptions.py +++ b/pype/services/rest_api/lib/exceptions.py @@ -1,4 +1,5 @@ class ObjAlreadyExist(Exception): + """Is used when is created multiple objects of same RestApi class.""" def __init__(self, cls=None, message=None): if not (cls and message): message = "RestApi object was created twice." diff --git a/pype/services/rest_api/lib/factory.py b/pype/services/rest_api/lib/factory.py index 58f7fa8f8a..3a24fe5ae8 100644 --- a/pype/services/rest_api/lib/factory.py +++ b/pype/services/rest_api/lib/factory.py @@ -10,6 +10,16 @@ log = Logger().get_logger("RestApiFactory") def prepare_fullpath(path, prefix): + """Concatenate registered path and prefix with right form. + + :param path: Registered url path for registered callback. + :type path: str, list + :param prefix: Registered and prepared url prefix. + :type prefix: str, None + :return: concatenated prefix and path in right form + :rtype: str + """ + if path and prefix: fullpath = "{}/{}".format(prefix, path).replace("//", "/") elif path: @@ -26,6 +36,16 @@ def prepare_fullpath(path, prefix): def prepare_regex_from_path(full_path): + """Prepare regex based on set path. + + When registered path do not contain dynamic keys regex is not set. + Dynamic keys are specified with "<" and ">" ("<{dynamic key}>"). + + :param full_path: Full url path (prefix + path) for registered callback. + :type full_path: str, list, None + :return: regex and keys of all groups in regex + :rtype: tuple(SRE_Pattern, list), tuple(None, None) + """ get_indexes_regex = "<[^< >]+>" all_founded_keys = re.findall(get_indexes_regex, full_path) if not all_founded_keys: @@ -46,8 +66,17 @@ def prepare_regex_from_path(full_path): def prepare_prefix(url_prefix): - if url_prefix is None: - url_prefix = "" + """Check if the url_prefix is set and is in correct form. + + Output is None when prefix is empty or "/". + + :param url_prefix: Registered prefix of registered callback. + :type url_prefix: str, list, None + :return: Url prefix of registered callback + :rtype: str, None + """ + if url_prefix is None or url_prefix.strip() == "/": + return None elif isinstance(url_prefix, (list, tuple)): url_prefix = "/".join(url_prefix) else: @@ -64,6 +93,18 @@ def prepare_prefix(url_prefix): def prepare_methods(methods, callback=None): + """Check and convert entered methods. + + String `methods` is converted to list. All values are converted to + `RestMethods` enum object. Invalid methods are ignored and printed out. + + :param methods: Contain rest api methods, when callback is called. + :type methods: str, list + :param callback: Registered callback, helps to identify where is invalid method. + :type callback: function, method, optional + :return: Valid methods + :rtype: list + """ invalid_methods = collections.defaultdict(list) if not methods: @@ -113,6 +154,7 @@ def prepare_methods(methods, callback=None): return _methods def prepare_callback_info(callback): + """Prepare data for callback handling when should be triggered.""" callback_info = inspect.getfullargspec(callback) callback_args = callback_info.args @@ -143,6 +185,11 @@ def prepare_callback_info(callback): class _RestApiFactory: + """Factory is used to store and prepare callbacks for requests. + + Should be created only one object used for all registered callbacks when + it is expected to run only one http server. + """ registered_objs = [] unprocessed_routes = [] unprocessed_statics = [] @@ -180,6 +227,7 @@ class _RestApiFactory: self.unprocessed_routes.append(route) def register_obj(self, obj): + """Register object for decorated methods in class definition""" self.registered_objs.append(obj) def register_statics(self, item): @@ -187,6 +235,14 @@ class _RestApiFactory: self.unprocessed_statics.append(item) def _prepare_route(self, route): + """Prepare data of registered callbacks for routes. + + Registration info are prepared to easy filter during handling + of requests. + + :param route: Contain all necessary info for filtering and handling callback for registered route. + :type route: dict + """ callback = route["callback"] methods = prepare_methods(route["methods"], callback) url_prefix = prepare_prefix(route["url_prefix"]) @@ -205,6 +261,13 @@ class _RestApiFactory: }) def prepare_registered(self): + """Iterate through all registered callbacks and statics and prepare them + + First are processed callbacks registered with decorators in classes by + registered objects. Remaining callbacks are filtered, it is checked if + methods has `__self__` or are defined in (it is expeted they + do not requise access to object) + """ for url_prefix, dir_path in self.unprocessed_statics: self._process_statics((url_prefix, dir_path)) dir_path = os.path.normpath(dir_path) diff --git a/pype/services/rest_api/lib/handler.py b/pype/services/rest_api/lib/handler.py index de6b5a9a44..cd5c0abe55 100644 --- a/pype/services/rest_api/lib/handler.py +++ b/pype/services/rest_api/lib/handler.py @@ -48,8 +48,15 @@ class Handler(http.server.SimpleHTTPRequestHandler): return self._handle_request(RestMethods.PATCH) def _handle_request(self, rest_method): - """Because processing is technically the same for now so it is used - the same way + """Handle request by registered callbacks and statics. + + Statics are only for GET method request. `_handle_statics` is called + when path is matching registered statics prefix. Callbacks are filtered + by method and their prefixes. When path is matching `_handle_callback` + is called. + + If any registered callback or statics match requested path 400 status + is responsed. And 500 when unexpected error happens. """ parsed_url = urlparse(self.path) path = parsed_url.path @@ -151,6 +158,18 @@ class Handler(http.server.SimpleHTTPRequestHandler): def _handle_callback_result(self, result, rest_method): + """Send response to request based on result of callback. + :param result: Result returned by callback. + :type result: None, bool, dict, list, CallbackResult + :param rest_method: Rest api method (GET, POST, etc.). + :type rest_method: RestMethods + + Response is based on result type: + - None, True - It is expected everything was OK, status 200. + - False - It is expected callback was not successful, status 400 + - dict, list - Result is send under "data" key of body, status 200 + - CallbackResult - object specify status and data + """ content_type = "application/json" status = HTTPStatus.OK success = True @@ -207,8 +226,17 @@ class Handler(http.server.SimpleHTTPRequestHandler): return body def _handle_callback(self, item, parsed_url, rest_method): + """Prepare data from request and trigger callback. - regex = item["regex"] + Data are loaded from body of request if there are any. + + :param item: Item stored during callback registration with all info. + :type item: dict + :param parsed_url: Url parsed with urllib (separated path, query, etc.). + :type parsed_url: ParseResult + :param rest_method: Rest api method (GET, POST, etc.). + :type rest_method: RestMethods + """ regex_keys = item["regex_keys"] url_data = None @@ -258,6 +286,7 @@ class Handler(http.server.SimpleHTTPRequestHandler): return callback(*args, **kwargs) def _handle_statics(self, dirpath, path): + """Return static file in response when file exist in registered destination.""" path = os.path.normpath(dirpath + path) ctype = self.guess_type(path) diff --git a/pype/services/rest_api/lib/lib.py b/pype/services/rest_api/lib/lib.py index 73cf13683a..4fab11469e 100644 --- a/pype/services/rest_api/lib/lib.py +++ b/pype/services/rest_api/lib/lib.py @@ -36,6 +36,7 @@ class RestMethods(enum.Enum): class CustomNone: + """Created object can be used as custom None (not equal to None)""" def __init__(self, name): self._name = name @@ -69,6 +70,7 @@ class UrlData(HandlerDict): pass class RequestData(HandlerDict): pass class Query(HandlerDict): + """Class for url query convert to dict and string""" def __init__(self, query): if isinstance(query, dict): pass @@ -80,6 +82,7 @@ class Query(HandlerDict): return urlencode(dict(self), doseq=True) class Fragment(HandlerDict): + """Class for url fragment convert to dict and string""" def __init__(self, fragment): if isinstance(fragment, dict): _fragment = fragment @@ -110,6 +113,26 @@ class Fragment(HandlerDict): return "&".join(items) class RequestInfo: + """Object that can be passed to callback as argument. + + Contain necessary data for handling request. + Object is created to single use and can be used similar to dict. + + :param url_data: Data collected from path when path with dynamic keys is matching. + :type url_data: dict, None + :param request_data: Data of body from request. + :type request_data: dict, None + :param query: Query from url path of reques. + :type query: str, None + :param fragment: Fragment from url path of reques. + :type fragment: str, None + :param params: Parems from url path of reques. + :type params: str, None + :param method: Method of request (GET, POST, etc.) + :type method: RestMethods + :param handler: Handler handling request from http server. + :type handler: Handler + """ def __init__( self, url_data, request_data, query, fragment, params, method, handler ): @@ -140,6 +163,21 @@ class RequestInfo: class CallbackResult: + """Can be used as return value of callback. + + It is possible to specify status code, success boolean, message and data + for specify head and body of request response. `abort` should be rather used + when result is error. + + :param status_code: Status code of result. + :type status_code: int + :param success: Success is key in body, may be used for handling response. + :type success: bool + :param message: Similar to success, message is key in body and may be used for handling response. + :type message: str, None + :param data: Data is also key for body in response. + :type data: dict, None + """ _data = {} def __init__( diff --git a/pype/services/rest_api/rest_api.py b/pype/services/rest_api/rest_api.py index 994632a1e9..2df666b45a 100644 --- a/pype/services/rest_api/rest_api.py +++ b/pype/services/rest_api/rest_api.py @@ -14,6 +14,70 @@ log = Logger().get_logger("RestApiServer") class RestApiServer: + """Rest Api allows to access statics or callbacks with http requests. + + To register statics use `register_statics`. + + To register callback use `register_callback` method or use `route` decorator. + `route` decorator should be used with not-class functions, it is possible + to use within class when inherits `RestApi` (defined in `base_class.py`) + or created object, with used decorator, is registered with `register_obj`. + + .. code-block:: python + @route("/username", url_prefix="/api", methods=["get"]) + def get_username(): + return {"username": getpass.getuser()} + + In that case response to `localhost:{port}/api/username` will be status + `200` with body including `{"data": {"username": getpass.getuser()}}` + + Callback may expect one argument which will be filled with request + info. Data object has attributes: `request_data`, `query`, + `fragment`, `params`, `method`, `handler`, `url_data`. + request_data - Data from request body if there are any. + query - query from url path (?identificator=name) + fragment - fragment from url path (#reset_credentials) + params - params from url path + method - request method (GET, POST, PUT, etc.) + handler - Handler object of HttpServer Handler + url_data - dynamic url keys from registered path with their values + + Dynamic url keys may be set with path argument. + .. code-block:: python + from rest_api import route + + all_projects = { + "Proj1": {"proj_data": []}, + "Proj2": {"proj_data": []}, + } + + @route("/projects/", url_prefix="/api", methods=["get"]) + def get_projects(request_info): + project_name = request_info.url_data["project_name"] + if not project_name: + return all_projects + return all_projects.get(project_name) + + This example should end with status 404 if project is not found. In that + case is best to use `abort` method. + + .. code-block:: python + from rest_api import abort + + @route("/projects/", url_prefix="/api", methods=["get"]) + def get_projects(request_info): + project_name = request_info.url_data["project_name"] + if not project_name: + return all_projects + + project = all_projects.get(project_name) + if not project: + abort(404, "Project \"{}\".format(project_name) was not found") + return project + + Callback may return many types. For more information read docstring of + `_handle_callback_result` defined in handler. + """ def __init__(self): self.qaction = None self.failed_icon = None @@ -91,7 +155,7 @@ class RestApiThread(QtCore.QThread): """ Listener for REST requests. It is possible to register callbacks for url paths. - Be careful about crossreferencing to different QThreads it is not allowed. + Be careful about crossreferencing to different Threads it is not allowed. """ def __init__(self, module, port):