added docstrings

This commit is contained in:
Jakub Trllo 2019-10-18 00:26:10 +02:00
parent 7d623a2bb5
commit 518885a39f
6 changed files with 254 additions and 6 deletions

View file

@ -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/<project_name>"
- 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)

View file

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

View file

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

View file

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

View file

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

View file

@ -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/<project_name>", 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/<project_name>", 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):