initial commit of rest api module with partially working rest api handling

This commit is contained in:
iLLiCiTiT 2019-10-14 19:07:52 +02:00
parent 262c186ba8
commit 1298bf73bf
8 changed files with 984 additions and 230 deletions

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 "<locals>" 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)

View file

@ -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()

View file

@ -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()

View file

@ -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()