diff --git a/pype/services/rest_api/__init__.py b/pype/services/rest_api/__init__.py index c11ecfd761..f0fddce0ee 100644 --- a/pype/services/rest_api/__init__.py +++ b/pype/services/rest_api/__init__.py @@ -1,4 +1,15 @@ from .rest_api import RestApiServer +from .base_class import RestApi, abort, route, register_statics +from .lib import ( + RestMethods, + UrlData, + RequestData, + Query, + Fragment, + Params, + Handler, + CallbackResult +) def tray_init(tray_widget, main_widget): diff --git a/pype/services/rest_api/base_class.py b/pype/services/rest_api/base_class.py new file mode 100644 index 0000000000..94c7eb0bbd --- /dev/null +++ b/pype/services/rest_api/base_class.py @@ -0,0 +1,68 @@ +from functools import wraps +from http import HTTPStatus + +from .lib import ( + RestApiFactory, Splitter, + ObjAlreadyExist, AbortException, + Params, UrlData, RequestData, Query, Fragment, Handler +) + + +def route(path, url_prefix="", methods=[]): + def decorator(callback): + @wraps(callback) + def wrapper(*args, **kwargs): + return callback(*args, **kwargs) + + func = wrapper + func.restapi = True + func.path = path + func.methods = methods + func.url_prefix = url_prefix + if hasattr(callback, "__self__"): + func.__self__ = callback.__self__ + func.callback = callback + + RestApiFactory.register_route(func) + return func + return decorator + + +def register_statics(url_prefix, dir_path): + RestApiFactory.register_statics((url_prefix, dir_path)) + + +def abort(status_code=HTTPStatus.NOT_FOUND, message=None): + items = [] + items.append(str(status_code)) + if not message: + message = "" + + items.append(message) + + raise AbortException(Splitter.join(items)) + + +class RestApi: + def route(path, url_prefix="", methods=[]): + return route(path, url_prefix, methods) + + @classmethod + def register_route(cls, callback, path, url_prefix="", methods=[]): + return route(path, methods, url_prefix)(callback) + + @classmethod + def register_statics(cls, url_prefix, dir_path): + return register_statics(url_prefix, dir_path) + + @classmethod + def abort(cls, status_code=HTTPStatus.NOT_FOUND, message=None): + abort(status_code, message) + + def __new__(cls, *args, **kwargs): + for obj in RestApiFactory.registered_objs: + if type(obj) == cls: + raise ObjAlreadyExist(cls) + instance = super(RestApi, cls).__new__(cls) + RestApiFactory.register_obj(instance) + return instance diff --git a/pype/services/rest_api/lib/__init__.py b/pype/services/rest_api/lib/__init__.py new file mode 100644 index 0000000000..38d476050a --- /dev/null +++ b/pype/services/rest_api/lib/__init__.py @@ -0,0 +1,19 @@ +Splitter = "__splitter__" + +from .exceptions import ObjAlreadyExist, AbortException +from .lib import ( + RestMethods, + CustomNone, + UrlData, + RequestData, + Query, + Fragment, + Params, + CallbackResult +) + +from .factory import _RestApiFactory + +RestApiFactory = _RestApiFactory() + +from .handler import Handler diff --git a/pype/services/rest_api/lib/exceptions.py b/pype/services/rest_api/lib/exceptions.py new file mode 100644 index 0000000000..849b522ddd --- /dev/null +++ b/pype/services/rest_api/lib/exceptions.py @@ -0,0 +1,10 @@ +class ObjAlreadyExist(Exception): + def __init__(self, cls=None, message=None): + if not (cls and message): + message = "RestApi object was created twice." + elif not message: + message = "{} object was created twice.".format(cls.__name__) + super().__init__(message) + + +class AbortException(Exception): pass diff --git a/pype/services/rest_api/lib/factory.py b/pype/services/rest_api/lib/factory.py new file mode 100644 index 0000000000..dbbd6ec5af --- /dev/null +++ b/pype/services/rest_api/lib/factory.py @@ -0,0 +1,266 @@ +import os +import re +import inspect +import collections +from .lib import RestMethods + +from pypeapp import Logger + +log = Logger().get_logger("RestApiFactory") + + +def prepare_fullpath(path, prefix): + if path and prefix: + fullpath = "{}/{}".format(prefix, path).replace("//", "/") + elif path: + fullpath = path + elif prefix: + fullpath = prefix + else: + fullpath = "/" + + if not fullpath.startswith("/"): + fullpath = "/{}".format(fullpath) + + return fullpath + + +def prepare_regex_from_path(full_path): + get_indexes_regex = "<[^< >]+>" + all_founded_keys = re.findall(get_indexes_regex, full_path) + if not all_founded_keys: + return None, None + + regex_path = full_path + keys = [] + for key in all_founded_keys: + replacement = "(?P{}\w+)".format(key) + keys.append(key.replace("<", "").replace(">", "")) + if full_path.endswith(key): + replacement = "?{}?".format(replacement) + regex_path = regex_path.replace(key, replacement) + + regex_path = "^{}$".format(regex_path) + + return re.compile(regex_path), keys + + +def prepare_prefix(url_prefix): + if url_prefix is None: + url_prefix = "" + elif isinstance(url_prefix, (list, tuple)): + url_prefix = "/".join(url_prefix) + else: + items = [part for part in url_prefix.split("/") if part] + url_prefix = "/".join(items) + + if not url_prefix: + return None + + if not url_prefix.startswith("/"): + url_prefix = "/{}".format(url_prefix) + + return url_prefix + + +def prepare_methods(methods, callback=None): + invalid_methods = collections.defaultdict(list) + + if not methods: + _methods = [RestMethods.GET] + elif isinstance(methods, str) or isinstance(methods, RestMethods): + _method = RestMethods.get(methods) + _methods = [] + if _method is None: + invalid_methods[methods].append(callback) + else: + _methods.append(_method) + + else: + _methods = [] + for method in methods: + found = False + _method = RestMethods.get(method) + if _method == None: + invalid_methods[methods].append(callback) + continue + + _methods.append(_method) + + for method, callbacks in invalid_methods.items(): + callback_info = "" + + callbacks = [cbk for cbk in callbacks if cbk] + if len(callbacks) > 0: + multiple_ind = "" + if len(callbacks) > 1: + multiple_ind = "s" + + callback_items = [] + for callback in callbacks: + callback_items.append("\"{}<{}>\"".format( + callback.__qualname__, callback.__globals__["__file__"] + )) + + callback_info = " with callback{} {}".format( + multiple_ind, "| ".join(callback_items) + ) + + log.warning( + ("Invalid RestApi method \"{}\"{}").format(method, callback_info) + ) + + return _methods + +def prepare_callback_info(_callback): + callback = _callback.callback + callback_info = inspect.getfullargspec(callback) + + callback_args = callback_info.args + callback_args_len = 0 + if callback_args: + callback_args_len = len(callback_args) + if ( + type(_callback).__name__ == "method" + ): + callback_args_len -= 1 + + defaults = callback_info.defaults + defaults_len = 0 + if defaults: + defaults_len = len(defaults) + + annotations = callback_info.annotations + + return { + "args": callback_args, + "args_len": callback_args_len, + "defaults": defaults, + "defaults_len": defaults_len, + "hasargs": callback_info.varargs is not None, + "haskwargs": callback_info.varkw is not None, + "annotations": annotations + } + + +class _RestApiFactory: + registered_objs = [] + unprocessed_routes = [] + unprocessed_statics = [] + + prepared_routes = { + method: collections.defaultdict(list) for method in RestMethods + } + prepared_statics = {} + + has_routes = False + + def has_handlers(self): + return (self.has_routes or self.prepared_statics) + + def _process_route(self, callback): + return self.unprocessed_routes.pop( + self.unprocessed_routes.index(callback) + ) + + def _process_statics(self, item): + return self.unprocessed_statics.pop( + self.unprocessed_statics.index(item) + ) + + def register_route(self, item): + log.debug("Registering callback for item \"{}\"".format( + item.__qualname__ + )) + self.unprocessed_routes.append(item) + + def register_obj(self, obj): + self.registered_objs.append(obj) + + def register_statics(self, item): + log.debug("Registering statics path \"{}\"".format(item)) + self.unprocessed_statics.append(item) + + def _prepare_route(self, callback): + methods = prepare_methods(callback.methods, callback) + url_prefix = prepare_prefix(callback.url_prefix) + fullpath = prepare_fullpath(callback.path, url_prefix) + regex, regex_keys = prepare_regex_from_path(fullpath) + callback_info = prepare_callback_info(callback) + + for method in methods: + self.has_routes = True + self.prepared_routes[method][url_prefix].append({ + "regex": regex, + "regex_keys": regex_keys, + "fullpath": fullpath, + "callback": callback, + "callback_info": callback_info + }) + + def prepare_registered(self): + for url_prefix, dir_path in self.unprocessed_statics: + self._process_statics((url_prefix, dir_path)) + dir_path = os.path.normpath(dir_path) + if not os.path.exists(dir_path): + log.warning( + "Directory path \"{}\" was not found".format(dir_path) + ) + continue + url_prefix = prepare_prefix(url_prefix) + self.prepared_statics[url_prefix] = dir_path + + for obj in self.registered_objs: + method_names = [ + attr for attr in dir(obj) + if inspect.ismethod(getattr(obj, attr)) + ] + for method_name in method_names: + method = obj.__getattribute__(method_name) + + for callback in self.unprocessed_routes: + if not ( + callback.__qualname__ == method.__qualname__ and + callback.__module__ == method.__module__ and + callback.__globals__["__file__"] == method.__globals__["__file__"] + ): + continue + + self._process_route(callback) + + if not hasattr(method, "restapi"): + continue + + if not method.restapi: + continue + + self._prepare_route(method) + break + + for callback in self.unprocessed_routes: + is_class_method = len(callback.__qualname__.split(".")) != 1 + if is_class_method: + missing_self = True + if hasattr(callback, "__self__"): + if callback.__self__ is not None: + missing_self = False + + if "" in callback.__qualname__: + pass + + elif missing_self: + log.warning(( + "Object of callback \"{}\" from \"{}\" is not" + " accessible for api. Register object or" + " register callback with already created object" + "(not with decorator in class).".format( + callback.__qualname__, + callback.__globals__["__file__"] + ) + )) + continue + + self._prepare_route(callback) + continue + + self._prepare_route(callback) diff --git a/pype/services/rest_api/lib/handler.py b/pype/services/rest_api/lib/handler.py new file mode 100644 index 0000000000..71215c1f2c --- /dev/null +++ b/pype/services/rest_api/lib/handler.py @@ -0,0 +1,399 @@ +import os +import re +import json +import traceback +import http.server +from http import HTTPStatus +from urllib.parse import urlparse + +from .lib import ( + RestMethods, CallbackResult, + UrlData, RequestData, Query, Fragment, Params +) +from .exceptions import AbortException +from . import RestApiFactory, CustomNone, Splitter + +from pypeapp import Logger + +log = Logger().get_logger("RestApiHandler") +NotSet = CustomNone("NotSet") + + +class Handler(http.server.SimpleHTTPRequestHandler): + # TODO fill will necessary statuses + default_messages = { + HTTPStatus.BAD_REQUEST: "Bad request", + HTTPStatus.NOT_FOUND: "Not found" + } + + statuses = { + "POST": { + "OK": 200, + "CREATED": 201 + }, + "PUT": { + "OK": 200, + "NO_CONTENT": 204 + } + } + def do_GET(self): + return self._handle_request(RestMethods.GET) + + def do_POST(self): + return self._handle_request(RestMethods.POST) + + def do_PUT(self): + return self._handle_request(RestMethods.PUT) + + def do_DELETE(self): + return self._handle_request(RestMethods.DELETE) + + def do_PATCH(self): + 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 + """ + parsed_url = urlparse(self.path) + path = parsed_url.path + + if rest_method is RestMethods.GET: + for prefix, dirpath in RestApiFactory.prepared_statics.items(): + if not path.startswith(prefix): + continue + _path = path[len(prefix):] + return self._handle_statics(dirpath, _path) + + matching_item = None + found_prefix = None + url_prefixes = RestApiFactory.prepared_routes[rest_method] + for url_prefix, items in url_prefixes.items(): + if matching_item is not None: + break + + if url_prefix is not None: + if not path.startswith(url_prefix): + continue + + found_prefix = url_prefix + + for item in items: + regex = item["regex"] + item_full_path = item["fullpath"] + if regex is None: + if path == item_full_path: + item["url_data"] = None + matching_item = item + break + + else: + found = re.match(regex, path) + if found: + item["url_data"] = found.groupdict() + matching_item = item + break + + if not matching_item: + if found_prefix is not None: + _path = path.replace(found_prefix, "") + if _path: + request_str = " \"{}\"".format(_path) + else: + request_str = "" + + message = "Invalid path request{} for prefix \"{}\"".format( + request_str, found_prefix + ) + else: + message = "Invalid path request \"{}\"".format(self.path) + log.debug(message) + self.send_error(HTTPStatus.BAD_REQUEST, message) + + return + + try: + log.debug("Triggering callback for path \"{}\"".format(path)) + + result = self._handle_callback(matching_item, parsed_url) + + return self._handle_callback_result(result, rest_method) + + except AbortException as exc: + status_code, message = str(exc).split(Splitter) + status_code = int(status_code) + if not message: + message = self.default_messages.get( + status_code, "UnexpectedError" + ) + + self.send_response(status_code) + self.send_header("Content-type", "text/html") + self.send_header("Content-Length", len(message)) + self.end_headers() + + self.wfile.write(message.encode()) + return message + + except Exception as exc: + log_message = "Unexpected Exception was raised (this is bug!)" + log.error(log_message, exc_info=True) + replace_helper = 0 + items = [log_message] + items += traceback.extract_tb(exc.__traceback__).format() + message = "\n".join(items) + + self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR) + self.send_header("Content-type", "text/html") + self.send_header("Content-Length", len(message)) + self.end_headers() + + self.wfile.write(message.encode()) + return message + + + def _handle_callback_result(self, result, rest_method): + content_type = "application/json" + status = HTTPStatus.OK + success = True + message = None + data = None + + body = None + # TODO better handling of results + if isinstance(result, CallbackResult): + status = result.status_code + body_dict = {} + for key, value in result.items(): + if value is not None: + body_dict[key] = value + body = json.dumps(body_dict) + + elif result in [None, True]: + status = HTTPStatus.OK + success = True + message = "{} request for \"{}\" passed".format( + rest_method, self.path + ) + + elif result is False: + status = HTTPStatus.BAD_REQUEST + success = False + + elif isinstance(result, (dict, list)): + status = HTTPStatus.OK + data = result + + if status == HTTPStatus.NO_CONTENT: + self.send_response(status) + self.end_headers() + return + + if not body: + body_dict = {"success": success} + if message: + body_dict["message"] = message + + if not data: + data = {} + + body_dict["data"] = data + body = json.dumps(body_dict) + + self.send_response(status) + self.send_header("Content-type", content_type) + self.send_header("Content-Length", len(body)) + self.end_headers() + + self.wfile.write(body.encode()) + return body + + def _handle_callback(self, item, parsed_url): + + regex = item["regex"] + regex_keys = item["regex_keys"] + + url_data = None + if regex_keys: + url_data = {key: None for key in regex_keys} + if item["url_data"]: + for key, value in item["url_data"].items(): + url_data[key] = value + + in_data = None + cont_len = self.headers.get("Content-Length") + if cont_len: + content_length = int(cont_len) + in_data_str = self.rfile.read(content_length) + if in_data_str: + in_data = json.loads(in_data_str) + + url_data = UrlData(url_data) + request_data = RequestData(in_data) + query = Query(parsed_url.query) + params = Params(parsed_url.params) + fragment = Fragment(parsed_url.fragment) + + callback = item["callback"] + callback_info = item["callback_info"] + + _args = callback_info["args"] + _args_len = callback_info["args_len"] + _defaults = callback_info["defaults"] + _defaults_len = callback_info["defaults_len"] + _has_args = callback_info["hasargs"] + _has_kwargs = callback_info["haskwargs"] + + _annotations = callback_info["annotations"] + + arg_index = 0 + if type(callback).__name__ == "method": + arg_index = 1 + + _kwargs = {arg: NotSet for arg in _args[arg_index:]} + _available_kwargs = { + "url_data": url_data, + "request_data": request_data, + "query": query, + "params": params, + "fragment": fragment, + "handler": self + } + if not regex: + _available_kwargs.pop("url_data") + + available_len = len(_available_kwargs) + if available_len < (_args_len - _defaults_len): + raise Exception(( + "Callback expects {} required positional arguments but {} are" + " available {}<{}>" + ).format( + _args_len, available_len, callback.__qualname__, + callback.__globals__.get("__file__", "unknown file") + )) + elif available_len < _args_len: + log.warning(( + "Handler \"{}\" will never fill all args of callback {}<{}>" + ).format( + self.__class__.__name__, + callback.__qualname__, + callback.__globals__.get("__file__", "unknown file") + )) + + if _args_len == 0: + if _has_args: + return callback(*_available_kwargs.values()) + if _has_kwargs: + return callback(**_available_kwargs) + else: + return callback() + + if _annotations: + for arg, argtype in _annotations.items(): + if argtype == Query: + key = "query" + elif argtype == Params: + key = "params" + elif argtype == Fragment: + key = "fragment" + elif argtype == Handler: + key = "handler" + elif argtype == UrlData: + key = "url_data" + elif argtype == RequestData: + key = "request_data" + else: + continue + + _kwargs[arg] = _available_kwargs[key] + _available_kwargs[key] = NotSet + + for key1, value1 in _kwargs.items(): + if value1 is not NotSet: + continue + has_values = False + for key2, value2 in _available_kwargs.items(): + if value2 is NotSet: + continue + has_values = True + _kwargs[key1] = value2 + _available_kwargs[key2] = NotSet + break + + if not has_values: + break + + _args = [] + kw_keys = [key for key in _kwargs.keys()] + for key in kw_keys: + value = _kwargs.pop(key) + if value is not NotSet: + _args.append(value) + continue + + if _has_args: + for key, value in _available_kwargs.items(): + if value is NotSet: + continue + _args.append(value) + + elif _has_kwargs: + for key, value in _available_kwargs.items(): + if value is NotSet: + continue + _kwargs[key] = value + + return callback(*_args, **_kwargs) + + def _handle_statics(self, dirpath, path): + path = os.path.normpath(dirpath + path) + + ctype = self.guess_type(path) + try: + file_obj = open(path, "rb") + except OSError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return None + + try: + file_stat = os.fstat(file_obj.fileno()) + # Use browser cache if possible + if ("If-Modified-Since" in self.headers + and "If-None-Match" not in self.headers): + # compare If-Modified-Since and time of last file modification + try: + ims = http.server.email.utils.parsedate_to_datetime( + self.headers["If-Modified-Since"]) + except (TypeError, IndexError, OverflowError, ValueError): + # ignore ill-formed values + pass + else: + if ims.tzinfo is None: + # obsolete format with no timezone, cf. + # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 + ims = ims.replace(tzinfo=datetime.timezone.utc) + if ims.tzinfo is datetime.timezone.utc: + # compare to UTC datetime of last modification + last_modif = datetime.datetime.fromtimestamp( + file_stat.st_mtime, datetime.timezone.utc) + # remove microseconds, like in If-Modified-Since + last_modif = last_modif.replace(microsecond=0) + + if last_modif <= ims: + self.send_response(HTTPStatus.NOT_MODIFIED) + self.end_headers() + file_obj.close() + return None + + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", ctype) + self.send_header("Content-Length", str(file_stat[6])) + self.send_header("Last-Modified", + self.date_time_string(file_stat.st_mtime)) + self.end_headers() + self.wfile.write(file_obj.read()) + return file_obj + except: + self.log.error("Failed to read data from file \"{}\"".format(path)) + finally: + file_obj.close() diff --git a/pype/services/rest_api/lib/lib.py b/pype/services/rest_api/lib/lib.py new file mode 100644 index 0000000000..4a9e41ce26 --- /dev/null +++ b/pype/services/rest_api/lib/lib.py @@ -0,0 +1,140 @@ +import os +import re +import enum +from http import HTTPStatus +from urllib.parse import urlencode, parse_qs + +from pypeapp import Logger + +log = Logger().get_logger("RestApiServer") + + +class RestMethods(enum.Enum): + GET = "GET" + POST = "POST" + PUT = "PUT" + PATCH = "PATCH" + DELETE = "DELETE" + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.value == other.value + + elif isinstance(other, str): + return self.value.lower() == other.lower() + return self.value == other + + def __hash__(self): + return enum.Enum.__hash__(self) + + @classmethod + def get(cls, name, default=None): + for meth in cls: + if meth == name: + return meth + return default + + +class CustomNone: + def __init__(self, name): + self._name = name + + def __bool__(self): + return False + + def __eq__(self, other): + if type(other) == type(self): + if other._name == self._name: + return True + return False + + def __str__(self): + return self._name + + def __repr__(self): + return self._name + + +class HandlerDict(dict): + def __init__(self, data=None, *args, **kwargs): + if not data: + data = {} + super().__init__(data, *args, **kwargs) + + def __repr__(self): + return "<{}> {}".format(self.__class__.__name__, str(dict(self))) + +class Params(HandlerDict): pass +class UrlData(HandlerDict): pass +class RequestData(HandlerDict): pass + +class Query(HandlerDict): + def __init__(self, query): + if isinstance(query, dict): + pass + else: + query = parse_qs(query) + super().__init__(query) + + def get_string(self): + return urlencode(dict(self), doseq=True) + +class Fragment(HandlerDict): + def __init__(self, fragment): + if isinstance(fragment, dict): + _fragment = fragment + else: + _fragment = {} + for frag in fragment.split("&"): + if not frag: + continue + items = frag.split("=") + + value = None + key = items[0] + if len(items) == 2: + value = items[1] + elif len(items) > 2: + value = "=".join(items[1:]) + + _fragment[key] = value + + super().__init__(_fragment) + + def get_string(self): + items = [] + for parts in dict(self).items(): + items.append( + "=".join([p for p in parts if p]) + ) + return "&".join(items) + + +class CallbackResult: + _data = {} + + def __init__( + self, status_code=HTTPStatus.OK, success=True, message=None, data=None, + **kwargs + ): + self.status_code = status_code + self._data = { + "success": success, + "message": message, + "data": data + } + for k, v in kwargs.items(): + self._data[k] = v + + def __getitem__(self, key): + return self._data[key] + + def __iter__(self): + for key in self._data: + yield key + + def get(self, key, default=None): + return self._data.get(key, default) + + def items(self): + return self._data.items() diff --git a/pype/services/rest_api/rest_api.py b/pype/services/rest_api/rest_api.py index 22823a5586..ed644f3e8b 100644 --- a/pype/services/rest_api/rest_api.py +++ b/pype/services/rest_api/rest_api.py @@ -1,260 +1,51 @@ import os -import json -import enum +import re import collections import threading -from inspect import signature import socket -import http.server -from http import HTTPStatus import socketserver - from Qt import QtCore +from .lib import RestApiFactory, Handler +from .base_class import route, register_statics from pypeapp import config, Logger log = Logger().get_logger("RestApiServer") -class RestMethods(enum.Enum): - GET = "GET" - POST = "POST" - PUT = "PUT" - PATCH = "PATCH" - DELETE = "DELETE" - - def __repr__(self): - return str(self.value) - - def __eq__(self, other): - if isinstance(other, str): - return self.value == other - return self == other - - def __hash__(self): - return enum.Enum.__hash__(self) - - def __str__(self): - return str(self.value) - - -class Handler(http.server.SimpleHTTPRequestHandler): - - def do_GET(self): - self.process_request(RestMethods.GET) - - def do_POST(self): - """Common code for POST. - - This trigger callbacks on specific paths. - - If request contain data and callback func has arg data are sent to - callback too. - - Send back return values of callbacks. - """ - self.process_request(RestMethods.POST) - - def process_request(self, rest_method): - """Because processing is technically the same for now so it is used - the same way - """ - in_data = None - cont_len = self.headers.get("Content-Length") - if cont_len: - content_length = int(cont_len) - in_data_str = self.rfile.read(content_length) - if in_data_str: - in_data = json.loads(in_data_str) - - registered_callbacks = self.server.registered_callbacks[rest_method] - - path_items = [part.lower() for part in self.path.split("/") if part] - request_path = "/".join(path_items) - callback = registered_callbacks.get(request_path) - result = None - if callback: - log.debug( - "Triggering callbacks for path \"{}\"".format(request_path) - ) - try: - params = signature(callback).parameters - if len(params) > 0 and in_data: - result = callback(in_data) - else: - result = callback() - - self.send_response(HTTPStatus.OK) - - if result in [None, True] or isinstance(result, (str, int)): - message = str(result) - if result in [None, True]: - message = "{} request for \"{}\" passed".format( - rest_method, self.path - ) - - self.handle_result(rest_method, message=message) - - return - - if isinstance(result, (dict, list)): - message = json.dumps(result).encode() - self.handle_result(rest_method, final_output=message) - - return - - except Exception: - message = "{} request for \"{}\" failed".format( - rest_method, self.path - ) - log.error(message, exc_info=True) - - self.send_response(HTTPStatus.BAD_REQUEST) - self.handle_result(rest_method, message=message, success=False) - - return - - self.handle_result(rest_method) - - else: - message = ( - "{} request for \"{}\" don't have registered callback" - ).format(rest_method, self.path) - log.debug(message) - - self.send_response(HTTPStatus.NOT_FOUND) - self.handle_result(rest_method, message=message, success=False) - - def handle_result( - self, rest_method, final_output=None, message=None, success=True, - content_type="application/json" - ): - self.send_header("Content-type", content_type) - if final_output: - output = final_output - else: - if not message: - output = json.dumps({ - "success": False, - "message": ( - "{} request for \"{}\" has unexpected result" - ).format(rest_method, self.path) - }).encode() - - else: - output = json.dumps({ - "success": success, "message": message - }).encode() - - - if isinstance(output, str): - self.send_header("Content-Length", len(output)) - - self.end_headers() - - if output: - self.wfile.write(output) - - -class AdditionalArgsTCPServer(socketserver.TCPServer): - def __init__(self, registered_callbacks, *args, **kwargs): - self.registered_callbacks = registered_callbacks - super(AdditionalArgsTCPServer, self).__init__(*args, **kwargs) - - -class RestApiServer(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. - """ - +class RestApiServer: def __init__(self): - super(RestApiServer, self).__init__() - self.registered_callbacks = { - RestMethods.GET: collections.defaultdict(list), - RestMethods.POST: collections.defaultdict(list), - RestMethods.PUT: collections.defaultdict(list), - RestMethods.PATCH: collections.defaultdict(list), - RestMethods.DELETE: collections.defaultdict(list) - } - self.qaction = None self.failed_icon = None self._is_running = False + try: - self.presets = config.get_presets().get( - "services", {}).get( - "rest_api", {} - ) + self.presets = config.get_presets()["services"]["rest_api"] except Exception: self.presets = {"default_port": 8011, "exclude_ports": []} + log.debug(( + "There are not set presets for RestApiModule." + " Using defaults \"{}\"" + ).format(str(self.presets))) - self.port = self.find_port() + port = self.find_port() + self.rest_api_thread = RestApiThread(self, port) + + statics_dir = os.path.sep.join([os.environ["PYPE_MODULE_ROOT"], "res"]) + self.register_statics("/res", statics_dir) def set_qaction(self, qaction, failed_icon): self.qaction = qaction self.failed_icon = failed_icon - def register_callback(self, path, callback, rest_method=RestMethods.POST): - if isinstance(path, (list, set)): - path = "/".join([part.lower() for part in path]) - elif isinstance(path, str): - path = "/".join( - [part.lower() for part in str(path).split("/") if part] - ) + def register_callback(self, path, callback, url_prefix="", methods=[]): + route(path, url_prefix, methods)(callback) - if isinstance(rest_method, str): - rest_method = str(rest_method).upper() + def register_statics(self, url_prefix, dir_path): + register_statics(url_prefix, dir_path) - if path in self.registered_callbacks[rest_method]: - log.error( - "Path \"{}\" has already registered callback.".format(path) - ) - return False - - log.debug( - "Registering callback for path \"{}\"".format(path) - ) - self.registered_callbacks[rest_method][path] = callback - - return True - - def tray_start(self): - self.start() - - @property - def is_running(self): - return self._is_running - - def stop(self): - self._is_running = False - - def run(self): - self._is_running = True - if not self.registered_callbacks: - log.info("Any registered callbacks for Rest Api server.") - return - - try: - log.debug( - "Running Rest Api server on URL:" - " \"http://localhost:{}\"".format(self.port) - ) - with AdditionalArgsTCPServer( - self.registered_callbacks, - ("", self.port), - Handler - ) as httpd: - while self._is_running: - httpd.handle_request() - except Exception: - log.warning( - "Rest Api Server service has failed", exc_info=True - ) - self._is_running = False - if self.qaction and self.failed_icon: - self.qaction.setIcon(self.failed_icon) + def register_obj(self, obj): + RestApiFactory.register_obj(obj) def find_port(self): start_port = self.presets["default_port"] @@ -276,3 +67,53 @@ class RestApiServer(QtCore.QThread): found_port ) return found_port + + def tray_start(self): + RestApiFactory.prepare_registered() + if not RestApiFactory.has_handlers(): + log.debug("There are not registered any handlers for RestApi") + return + self.rest_api_thread.start() + + @property + def is_running(self): + return self.rest_api_thread.is_running + + def stop(self): + self.rest_api_thread.is_running = False + + def thread_stopped(self): + self._is_running = False + + +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. + """ + + def __init__(self, module, port): + super(RestApiThread, self).__init__() + self.is_running = False + self.module = module + self.port = port + + def run(self): + self.is_running = True + + try: + log.debug( + "Running Rest Api server on URL:" + " \"http://localhost:{}\"".format(self.port) + ) + with socketserver.TCPServer(("", self.port), Handler) as httpd: + while self.is_running: + httpd.handle_request() + except Exception: + log.warning( + "Rest Api Server service has failed", exc_info=True + ) + + self.is_running = False + self.module.thread_stopped()