mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
added docstrings
This commit is contained in:
parent
7d623a2bb5
commit
518885a39f
6 changed files with 254 additions and 6 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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__(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue