Merge pull request #1119 from pypeclub/feature/remove_rest_api_module

Remove Rest Api module
This commit is contained in:
Milan Kolar 2021-03-12 13:31:12 +01:00 committed by GitHub
commit d36cdb10a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 379 additions and 1485 deletions

View file

@ -274,9 +274,9 @@ class CreateRender(plugin.Creator):
# authentication token expired so we need to login to Muster
# again to get it. We use Pype API call to show login window.
api_url = "{}/muster/show_login".format(
os.environ["PYPE_REST_API_URL"])
os.environ["PYPE_WEBSERVER_URL"])
self.log.debug(api_url)
login_response = self._requests_post(api_url, timeout=1)
login_response = self._requests_get(api_url, timeout=1)
if login_response.status_code != 200:
self.log.error("Cannot show login form to Muster")
raise Exception("Cannot show login form to Muster")

View file

@ -191,9 +191,9 @@ class CreateVRayScene(plugin.Creator):
# authentication token expired so we need to login to Muster
# again to get it. We use Pype API call to show login window.
api_url = "{}/muster/show_login".format(
os.environ["PYPE_REST_API_URL"])
os.environ["PYPE_WEBSERVER_URL"])
self.log.debug(api_url)
login_response = self._requests_post(api_url, timeout=1)
login_response = self._requests_get(api_url, timeout=1)
if login_response.status_code != 200:
self.log.error("Cannot show login form to Muster")
raise Exception("Cannot show login form to Muster")

View file

@ -85,9 +85,9 @@ class ValidateMusterConnection(pyblish.api.ContextPlugin):
Renew authentication token by logging into Muster
"""
api_url = "{}/muster/show_login".format(
os.environ["PYPE_REST_API_URL"])
os.environ["PYPE_WEBSERVER_URL"])
cls.log.debug(api_url)
response = cls._requests_post(api_url, timeout=1)
response = cls._requests_get(api_url, timeout=1)
if response.status_code != 200:
cls.log.error('Cannot show login form to Muster')
raise Exception('Cannot show login form to Muster')

View file

@ -168,7 +168,7 @@ def clearing_caches_ui():
def test_rest_api_server(env):
# from pprint import pformat
rest_url = env.get("PYPE_REST_API_URL")
rest_url = env.get("PYPE_WEBSERVER_URL")
project_name = "{AVALON_PROJECT}".format(**env)
URL = "/".join((rest_url,
"avalon/projects",

View file

@ -14,7 +14,7 @@ class PypeRestApiClient {
* @return {url string}
*/
_getApiServerUrl() {
var url = this.env.PYPE_REST_API_URL;
var url = this.env.PYPE_WEBSERVER_URL;
return url
}

View file

@ -13,9 +13,9 @@ from .settings_action import (
SettingsAction,
LocalSettingsAction
)
from .rest_api import (
RestApiModule,
IRestApi
from .webserver import (
WebServerModule,
IWebServerRoutes
)
from .user import (
UserModule,
@ -40,7 +40,6 @@ from .log_viewer import LogViewModule
from .muster import MusterModule
from .deadline import DeadlineModule
from .standalonepublish_action import StandAlonePublishAction
from .webserver import WebServerModule
from .sync_server import SyncServer
@ -57,6 +56,9 @@ __all__ = (
"SettingsAction",
"LocalSettingsAction",
"WebServerModule",
"IWebServerRoutes",
"UserModule",
"IUserModule",
@ -66,9 +68,6 @@ __all__ = (
"TimersManager",
"ITimersManager",
"RestApiModule",
"IRestApi",
"AvalonModule",
"LauncherAction",
@ -82,6 +81,5 @@ __all__ = (
"DeadlineModule",
"StandAlonePublishAction",
"WebServerModule",
"SyncServer"
)

View file

@ -4,11 +4,11 @@ from pype import resources
from .. import (
PypeModule,
ITrayModule,
IRestApi
IWebServerRoutes
)
class AvalonModule(PypeModule, ITrayModule, IRestApi):
class AvalonModule(PypeModule, ITrayModule, IWebServerRoutes):
name = "avalon"
def initialize(self, modules_settings):
@ -74,10 +74,12 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi):
def connect_with_modules(self, _enabled_modules):
return
def rest_api_initialization(self, rest_api_module):
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import AvalonRestApi
self.rest_api_obj = AvalonRestApi()
from .rest_api import AvalonRestApiResource
self.rest_api_obj = AvalonRestApiResource(self, server_manager)
# Definition of Tray menu
def tray_menu(self, tray_menu):

View file

@ -1,85 +1,146 @@
import os
import re
import json
import datetime
import bson
from bson.objectid import ObjectId
import bson.json_util
from pype.modules.rest_api import RestApi, abort, CallbackResult
from aiohttp.web_response import Response
from avalon.api import AvalonMongoDB
from pype.modules.webserver.base_routes import RestApiEndpoint
class AvalonRestApi(RestApi):
class _RestApiEndpoint(RestApiEndpoint):
def __init__(self, resource):
self.resource = resource
super(_RestApiEndpoint, self).__init__()
@property
def dbcon(self):
return self.resource.dbcon
class AvalonProjectsEndpoint(_RestApiEndpoint):
async def get(self) -> Response:
output = []
for project_name in self.dbcon.database.collection_names():
project_doc = self.dbcon.database[project_name].find_one({
"type": "project"
})
output.append(project_doc)
return Response(
status=200,
body=self.resource.encode(output),
content_type="application/json"
)
class AvalonProjectEndpoint(_RestApiEndpoint):
async def get(self, project_name) -> Response:
project_doc = self.dbcon.database[project_name].find_one({
"type": "project"
})
if project_doc:
return Response(
status=200,
body=self.resource.encode(project_doc),
content_type="application/json"
)
return Response(
status=404,
reason="Project name {} not found".format(project_name)
)
class AvalonAssetsEndpoint(_RestApiEndpoint):
async def get(self, project_name) -> Response:
asset_docs = list(self.dbcon.database[project_name].find({
"type": "asset"
}))
return Response(
status=200,
body=self.resource.encode(asset_docs),
content_type="application/json"
)
class AvalonAssetEndpoint(_RestApiEndpoint):
async def get(self, project_name, asset_name) -> Response:
asset_doc = self.dbcon.database[project_name].find_one({
"type": "asset",
"name": asset_name
})
if asset_doc:
return Response(
status=200,
body=self.resource.encode(asset_doc),
content_type="application/json"
)
return Response(
status=404,
reason="Asset name {} not found in project {}".format(
asset_name, project_name
)
)
class AvalonRestApiResource:
def __init__(self, avalon_module, server_manager):
self.module = avalon_module
self.server_manager = server_manager
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dbcon = AvalonMongoDB()
self.dbcon.install()
@RestApi.route("/projects/<project_name>", url_prefix="/avalon", methods="GET")
def get_project(self, request):
project_name = request.url_data["project_name"]
if not project_name:
output = {}
for project_name in self.dbcon.tables():
project = self.dbcon[project_name].find_one({
"type": "project"
})
output[project_name] = project
self.prefix = "/avalon"
return CallbackResult(data=self.result_to_json(output))
self.endpoint_defs = (
(
"GET",
"/projects",
AvalonProjectsEndpoint(self)
),
(
"GET",
"/projects/{project_name}",
AvalonProjectEndpoint(self)
),
(
"GET",
"/projects/{project_name}/assets",
AvalonAssetsEndpoint(self)
),
(
"GET",
"/projects/{project_name}/assets/{asset_name}",
AvalonAssetEndpoint(self)
)
)
project = self.dbcon[project_name].find_one({"type": "project"})
self.register()
if project:
return CallbackResult(data=self.result_to_json(project))
def register(self):
for methods, url, endpoint in self.endpoint_defs:
final_url = self.prefix + url
self.server_manager.add_route(
methods, final_url, endpoint.dispatch
)
abort(404, "Project \"{}\" was not found in database".format(
project_name
))
@staticmethod
def json_dump_handler(value):
if isinstance(value, datetime.datetime):
return value.isoformat()
if isinstance(value, ObjectId):
return str(value)
raise TypeError(value)
@RestApi.route("/projects/<project_name>/assets/<asset>", url_prefix="/avalon", methods="GET")
def get_assets(self, request):
_project_name = request.url_data["project_name"]
_asset = request.url_data["asset"]
if not self.dbcon.exist_table(_project_name):
abort(404, "Project \"{}\" was not found in database".format(
_project_name
))
if not _asset:
assets = self.dbcon[_project_name].find({"type": "asset"})
output = self.result_to_json(assets)
return CallbackResult(data=output)
# identificator can be specified with url query (default is `name`)
identificator = request.query.get("identificator", "name")
asset = self.dbcon[_project_name].find_one({
"type": "asset",
identificator: _asset
})
if asset:
id = asset["_id"]
asset["_id"] = str(id)
return asset
abort(404, "Asset \"{}\" with {} was not found in project {}".format(
_asset, identificator, _project_name
))
def result_to_json(self, result):
""" Converts result of MongoDB query to dict without $oid (ObjectId)
keys with help of regex matching.
..note:
This will convert object type entries similar to ObjectId.
"""
bson_json = bson.json_util.dumps(result)
# Replace "{$oid: "{entity id}"}" with "{entity id}"
regex1 = '(?P<id>{\"\$oid\": \"[^\"]+\"})'
regex2 = '{\"\$oid\": (?P<id>\"[^\"]+\")}'
for value in re.findall(regex1, bson_json):
for substr in re.findall(regex2, value):
bson_json = bson_json.replace(value, substr)
return json.loads(bson_json)
@classmethod
def encode(cls, data):
return json.dumps(
data,
indent=4,
default=cls.json_dump_handler
).encode("utf-8")

View file

@ -2,10 +2,14 @@ import os
import json
import appdirs
import requests
from .. import PypeModule, ITrayModule, IRestApi
from .. import (
PypeModule,
ITrayModule,
IWebServerRoutes
)
class MusterModule(PypeModule, ITrayModule, IRestApi):
class MusterModule(PypeModule, ITrayModule, IWebServerRoutes):
"""
Module handling Muster Render credentials. This will display dialog
asking for user credentials for Muster if not already specified.
@ -28,6 +32,7 @@ class MusterModule(PypeModule, ITrayModule, IRestApi):
# Tray attributes
self.widget_login = None
self.action_show_login = None
self.rest_api_obj = None
def get_global_environments(self):
return {
@ -71,13 +76,12 @@ class MusterModule(PypeModule, ITrayModule, IRestApi):
parent.addMenu(menu)
def rest_api_initialization(self, rest_api_module):
"""Implementation of IRestApi interface."""
def api_show_login():
self.action_show_login.trigger()
rest_api_module.register_callback(
"/show_login", api_show_login, "muster", "post"
)
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import MusterModuleRestApi
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
def load_credentials(self):
"""

View file

@ -0,0 +1,22 @@
from aiohttp.web_response import Response
class MusterModuleRestApi:
def __init__(self, user_module, server_manager):
self.module = user_module
self.server_manager = server_manager
self.prefix = "/muster"
self.register()
def register(self):
self.server_manager.add_route(
"GET",
self.prefix + "/show_login",
self.show_login_widget
)
async def show_login_widget(self, request):
self.module.action_show_login.trigger()
return Response(status=200)

View file

@ -1,27 +0,0 @@
from .rest_api import (
RestApiModule,
IRestApi
)
from .base_class import (
RestApi,
abort,
route,
register_statics
)
from .lib import (
RestMethods,
CallbackResult
)
__all__ = (
"RestApiModule",
"IRestApi",
"RestApi",
"abort",
"route",
"register_statics",
"RestMethods",
"CallbackResult"
)

View file

@ -1,124 +0,0 @@
from http import HTTPStatus
from .lib import (
RestApiFactory, Splitter,
ObjAlreadyExist, AbortException,
)
def route(path, url_prefix="", methods=[], strict_match=False):
"""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, etc.) when
callback will be triggered, defaults to ["GET"]
:type methods: list, str, optional
:param strict_match: Decides if callback can handle both single and
multiple entities (~/projects/<project_name> && ~/projects/),
defaults to False.
:type strict_match: bool
`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, strict_match
)
callback.restapi = True
return callback
return decorator
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:
message = ""
items.append(message)
raise AbortException(Splitter.join(items))
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=[], strict_match=False):
return route(path, url_prefix, methods, strict_match)
@classmethod
def register_route(
cls, callback, path, url_prefix="", methods=[], strict_match=False
):
return route(path, methods, url_prefix, strict_match)(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

@ -1,7 +0,0 @@
from .exceptions import ObjAlreadyExist, AbortException
from .lib import RestMethods, CallbackResult, RequestInfo, Splitter
from .factory import _RestApiFactory
RestApiFactory = _RestApiFactory()
from .handler import Handler

View file

@ -1,12 +0,0 @@
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."
elif not message:
message = "{} object was created twice.".format(cls.__name__)
super().__init__(message)
class AbortException(Exception):
pass

View file

@ -1,357 +0,0 @@
import os
import re
import inspect
import collections
from .lib import RestMethods
from queue import Queue
from pype.api import Logger
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 isinstance(path, (list, tuple)):
path_items = path
else:
path_items = [part for part in path.split("/") if part]
fullpath = "/"
if path and prefix:
items = [part for part in prefix.split("/") if part]
items.extend(path_items)
fullpath = "/".join(items)
if path.endswith("/"):
fullpath += "/"
elif path:
fullpath = "/".join(path_items)
if path.endswith("/"):
fullpath += "/"
elif prefix:
fullpath = prefix
if not fullpath.startswith("/"):
fullpath = "/{}".format(fullpath)
return fullpath
def prepare_regex_from_path(full_path, strict_match):
"""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:
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 not strict_match:
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):
"""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:
items = [part for part in url_prefix.split("/") if part]
url_prefix = "/".join(items)
if not url_prefix:
return None
while url_prefix.endswith("/"):
url_prefix = url_prefix[:-1]
if not url_prefix.startswith("/"):
url_prefix = "/{}".format(url_prefix)
return 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:
_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 is 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):
"""Prepare data for callback handling when should be triggered."""
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:
"""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 = Queue()
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, route):
return self.unprocessed_routes.pop(
self.unprocessed_routes.index(route)
)
def register_route(
self, path, callback, url_prefix, methods, strict_match
):
log.debug("Registering callback for item \"{}\"".format(
callback.__qualname__
))
route = {
"path": path,
"callback": callback,
"url_prefix": url_prefix,
"methods": methods,
"strict_match": strict_match
}
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):
log.debug("Registering statics path \"{}\"".format(item))
self.unprocessed_statics.put(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"])
fullpath = prepare_fullpath(route["path"], url_prefix)
regex, regex_keys = prepare_regex_from_path(
fullpath, route["strict_match"]
)
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):
"""Iter through all registered callbacks and statics to 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)
"""
while not self.unprocessed_statics.empty():
url_prefix, dir_path = self.unprocessed_statics.get()
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)
if not hasattr(method, "restapi"):
continue
if not method.restapi:
continue
for route in list(self.unprocessed_routes):
callback = route["callback"]
if not (
callback.__qualname__ == method.__qualname__ and
callback.__module__ == method.__module__ and
callback.__globals__["__file__"] == (
method.__globals__["__file__"]
)
):
continue
route["callback"] = method
self._process_route(route)
self._prepare_route(route)
break
for route in list(self.unprocessed_routes):
callback = route["callback"]
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(route)
continue
self._prepare_route(route)

View file

@ -1,349 +0,0 @@
import os
import re
import json
import datetime
import traceback
import http.server
from http import HTTPStatus
from urllib.parse import urlparse
from .lib import RestMethods, CallbackResult, RequestInfo
from .exceptions import AbortException
from . import RestApiFactory, Splitter
from pype.api import Logger
log = Logger().get_logger("RestApiHandler")
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):
"""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
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, rest_method
)
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)
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):
"""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
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, rest_method):
"""Prepare data from request and trigger callback.
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
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:
try:
in_data = json.loads(in_data_str)
except Exception as e:
log.error("Invalid JSON recieved: \"{}\"".format(
str(in_data_str)
))
raise Exception("Invalid JSON recieved") from e
request_info = RequestInfo(
url_data=url_data,
request_data=in_data,
query=parsed_url.query,
fragment=parsed_url.fragment,
params=parsed_url.params,
method=rest_method,
handler=self
)
callback = item["callback"]
callback_info = item["callback_info"]
_args_len = callback_info["args_len"]
_has_args = callback_info["hasargs"]
_has_kwargs = callback_info["haskwargs"]
args = []
kwargs = {}
if _args_len == 0:
if _has_args:
args.append(request_info)
elif _has_kwargs:
kwargs["request_info"] = request_info
else:
args.append(request_info)
return callback(*args, **kwargs)
def _handle_statics(self, dirpath, path):
"""Return static file in response when file exist in destination."""
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 Exception:
log.error(
"Failed to read data from file \"{}\"".format(path),
exc_info=True
)
finally:
file_obj.close()

View file

@ -1,197 +0,0 @@
import enum
from http import HTTPStatus
from urllib.parse import urlencode, parse_qs
Splitter = "__splitter__"
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 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):
"""Class for url query convert to dict and string."""
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):
"""Class for url fragment convert to dict and string."""
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 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
):
self.url_data = UrlData(url_data)
self.request_data = RequestData(request_data)
self.query = Query(query)
self.fragment = Fragment(fragment)
self.params = Params(params)
self.method = method
self.handler = handler
def __getitem__(self, key):
return self.__getattribute__(key)
def __hash__(self):
return {
"url_data": self.url_data,
"request_data": self. request_data,
"query": self.query,
"fragment": self.fragment,
"params": self.params,
"method": self.method,
"handler": self.handler
}
def items(self):
return dict(self).items()
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__(
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,258 +0,0 @@
import os
import socket
import threading
from abc import ABCMeta, abstractmethod
from socketserver import ThreadingMixIn
from http.server import HTTPServer
import six
from pype.lib import PypeLogger
from pype import resources
from .lib import RestApiFactory, Handler
from .base_class import route, register_statics
from .. import PypeModule, ITrayService
@six.add_metaclass(ABCMeta)
class IRestApi:
"""Other modules interface to return paths to ftrack event handlers.
Expected output is dictionary with "server" and "user" keys.
"""
@abstractmethod
def rest_api_initialization(self, rest_api_module):
pass
class RestApiModule(PypeModule, ITrayService):
"""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"], strict_match=False)
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"], strict_match=False)
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"], strict_match=False)
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
`strict_match` allows to handle not only specific entity but all entity types.
E.g. "/projects/<project_name>" with set `strict_match` to False will handle also
"/projects" or "/projects/" path. It is necessary to set `strict_match` to
True when should handle only single entity.
Callback may return many types. For more information read docstring of
`_handle_callback_result` defined in handler.
"""
label = "Rest API Service"
name = "rest_api"
def initialize(self, modules_settings):
rest_api_settings = modules_settings[self.name]
self.enabled = True
self.default_port = rest_api_settings["default_port"]
self.exclude_ports = rest_api_settings["exclude_ports"]
self.rest_api_url = None
self.rest_api_thread = None
def register_callback(
self, path, callback, url_prefix="", methods=[], strict_match=False
):
RestApiFactory.register_route(
path, callback, url_prefix, methods, strict_match
)
def register_statics(self, url_prefix, dir_path):
register_statics(url_prefix, dir_path)
def register_obj(self, obj):
RestApiFactory.register_obj(obj)
def connect_with_modules(self, enabled_modules):
# Do not register restapi callbacks out of tray
if self.tray_initialized:
for module in enabled_modules:
if not isinstance(module, IRestApi):
continue
module.rest_api_initialization(self)
@staticmethod
def find_free_port(port_from, port_to=None, exclude_ports=None, host=None):
"""Find available socket port from entered range.
It is also possible to only check if entered port is available.
Args:
port_from (int): Port number which is checked as first.
port_to (int): Last port that is checked in sequence from entered
`port_from`. Only `port_from` is checked if is not entered.
Nothing is processed if is equeal to `port_from`!
exclude_ports (list, tuple, set): List of ports that won't be
checked form entered range.
host (str): Host where will check for free ports. Set to
"localhost" by default.
"""
# Check only entered port if `port_to` is not defined
if port_to is None:
port_to = port_from
# Excluded ports (e.g. reserved for other servers/clients)
if exclude_ports is None:
exclude_ports = []
# Default host is localhost but it is possible to look for other hosts
if host is None:
host = "localhost"
found_port = None
for port in range(port_from, port_to + 1):
if port in exclude_ports:
continue
sock = None
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
found_port = port
except socket.error:
continue
finally:
if sock:
sock.close()
if found_port is not None:
break
return found_port
def tray_init(self):
port = self.find_free_port(
self.default_port, self.default_port + 100, self.exclude_ports
)
self.rest_api_url = "http://localhost:{}".format(port)
self.rest_api_thread = RestApiThread(self, port)
self.register_statics("/res", resources.RESOURCES_DIR)
# Set rest api environments
os.environ["PYPE_REST_API_URL"] = self.rest_api_url
def tray_start(self):
RestApiFactory.prepare_registered()
if not RestApiFactory.has_handlers():
self.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 tray_exit(self):
self.stop()
def stop(self):
self.rest_api_thread.stop()
self.rest_api_thread.join()
class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
pass
class RestApiThread(threading.Thread):
""" Listener for REST requests.
It is possible to register callbacks for url paths.
Be careful about crossreferencing to different Threads it is not allowed.
"""
def __init__(self, module, port):
super(RestApiThread, self).__init__()
self.is_running = False
self.module = module
self.port = port
self.httpd = None
self.log = PypeLogger().get_logger("RestApiThread")
def stop(self):
self.is_running = False
if self.httpd:
self.httpd.server_close()
def run(self):
self.is_running = True
try:
self.log.debug(
"Running Rest Api server on URL:"
" \"http://localhost:{}\"".format(self.port)
)
with ThreadingSimpleServer(("", self.port), Handler) as httpd:
self.httpd = httpd
while self.is_running:
httpd.handle_request()
except Exception:
self.log.warning(
"Rest Api Server service has failed", exc_info=True
)
self.httpd = None
self.is_running = False

View file

@ -0,0 +1,35 @@
import json
from aiohttp.web_response import Response
class UserModuleRestApi:
def __init__(self, user_module, server_manager):
self.module = user_module
self.server_manager = server_manager
self.prefix = "/user"
self.register()
def register(self):
self.server_manager.add_route(
"GET",
self.prefix + "/username",
self.get_username
)
self.server_manager.add_route(
"GET",
self.prefix + "/show_widget",
self.show_user_widget
)
async def get_username(self, request):
return Response(
status=200,
body=json.dumps(self.module.cred, indent=4),
content_type="application/json"
)
async def show_user_widget(self, request):
self.module.action_show_widget.trigger()
return Response(status=200)

View file

@ -7,7 +7,11 @@ from abc import ABCMeta, abstractmethod
import six
import appdirs
from .. import PypeModule, ITrayModule, IRestApi
from .. import (
PypeModule,
ITrayModule,
IWebServerRoutes
)
@six.add_metaclass(ABCMeta)
@ -20,7 +24,7 @@ class IUserModule:
pass
class UserModule(PypeModule, ITrayModule, IRestApi):
class UserModule(PypeModule, ITrayModule, IWebServerRoutes):
cred_folder_path = os.path.normpath(
appdirs.user_data_dir('pype-app', 'pype')
)
@ -43,6 +47,8 @@ class UserModule(PypeModule, ITrayModule, IRestApi):
self.widget_login = None
self.action_show_widget = None
self.rest_api_obj = None
def tray_init(self):
from .widget_user import UserWidget
self.widget_login = UserWidget(self)
@ -68,20 +74,11 @@ class UserModule(PypeModule, ITrayModule, IRestApi):
def get_user(self):
return self.cred.get("username") or getpass.getuser()
def rest_api_initialization(self, rest_api_module):
def api_get_username():
return self.cred
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
from .rest_api import UserModuleRestApi
rest_api_module.register_callback(
"user/username", api_get_username, "get"
)
def api_show_widget():
self.action_show_widget.trigger()
rest_api_module.register_callback(
"user/show_widget", api_show_widget, "post"
)
self.rest_api_obj = UserModuleRestApi(self, server_manager)
def connect_with_modules(self, enabled_modules):
for module in enabled_modules:

View file

@ -1,6 +1,10 @@
from .webserver_module import WebServerModule
from .webserver_module import (
WebServerModule,
IWebServerRoutes
)
__all__ = (
"WebServerModule",
"IWebServerRoutes"
)

View file

@ -0,0 +1,50 @@
"""Helper functions or classes for Webserver module.
These must not be imported in module itself to not break Python 2
applications.
"""
import inspect
from aiohttp.http_exceptions import HttpBadRequest
from aiohttp.web_exceptions import HTTPMethodNotAllowed
from aiohttp.web_request import Request
DEFAULT_METHODS = ("GET", "POST", "PUT", "DELETE")
class RestApiEndpoint:
"""Helper endpoint class for single endpoint.
Class can define `get`, `post`, `put` or `delete` async methods for the
endpoint.
"""
def __init__(self):
methods = {}
for method_name in DEFAULT_METHODS:
method = getattr(self, method_name.lower(), None)
if method:
methods[method_name.upper()] = method
self.methods = methods
async def dispatch(self, request: Request):
method = self.methods.get(request.method.upper())
if not method:
raise HTTPMethodNotAllowed("", DEFAULT_METHODS)
wanted_args = list(inspect.signature(method).parameters.keys())
available_args = request.match_info.copy()
available_args["request"] = request
unsatisfied_args = set(wanted_args) - set(available_args.keys())
if unsatisfied_args:
# Expected match info that doesn't exist
raise HttpBadRequest("")
return await method(**{
arg_name: available_args[arg_name]
for arg_name in wanted_args
})

View file

@ -21,7 +21,11 @@ class WebServerManager:
# add route with multiple methods for single "external app"
self.webserver_thread = WebServerThread(self, self.module.port)
self.webserver_thread = WebServerThread(self)
@property
def port(self):
return self.module.port
def add_route(self, *args, **kwargs):
self.app.router.add_route(*args, **kwargs)
@ -60,17 +64,20 @@ class WebServerManager:
class WebServerThread(threading.Thread):
""" Listener for requests in thread."""
def __init__(self, manager, port):
def __init__(self, manager):
super(WebServerThread, self).__init__()
self.is_running = False
self.port = port
self.manager = manager
self.loop = None
self.runner = None
self.site = None
self.tasks = []
@property
def port(self):
return self.manager.port
def run(self):
self.is_running = True

View file

@ -1,21 +1,38 @@
import os
import socket
from abc import ABCMeta, abstractmethod
import six
from pype import resources
from .. import PypeModule, ITrayService
@six.add_metaclass(ABCMeta)
class IWebServerRoutes:
"""Other modules interface to register their routes."""
@abstractmethod
def webserver_initialization(self, server_manager):
pass
class WebServerModule(PypeModule, ITrayService):
name = "webserver"
label = "WebServer"
def initialize(self, module_settings):
def initialize(self, _module_settings):
self.enabled = True
self.server_manager = None
# TODO find free port
self.port = 8098
self.port = self.find_free_port()
def connect_with_modules(self, *_a, **_kw):
return
def connect_with_modules(self, enabled_modules):
if not self.server_manager:
return
for module in enabled_modules:
if isinstance(module, IWebServerRoutes):
module.webserver_initialization(self.server_manager)
def tray_init(self):
self.create_server_manager()
@ -31,8 +48,10 @@ class WebServerModule(PypeModule, ITrayService):
static_prefix = "/res"
self.server_manager.add_static(static_prefix, resources.RESOURCES_DIR)
os.environ["PYPE_STATICS_SERVER"] = "http://localhost:{}{}".format(
self.port, static_prefix
webserver_url = "http://localhost:{}".format(self.port)
os.environ["PYPE_WEBSERVER_URL"] = webserver_url
os.environ["PYPE_STATICS_SERVER"] = "{}{}".format(
webserver_url, static_prefix
)
def start_server(self):
@ -53,3 +72,58 @@ class WebServerModule(PypeModule, ITrayService):
self.server_manager.on_stop_callbacks.append(
self.set_service_failed_icon
)
@staticmethod
def find_free_port(
port_from=None, port_to=None, exclude_ports=None, host=None
):
"""Find available socket port from entered range.
It is also possible to only check if entered port is available.
Args:
port_from (int): Port number which is checked as first.
port_to (int): Last port that is checked in sequence from entered
`port_from`. Only `port_from` is checked if is not entered.
Nothing is processed if is equeal to `port_from`!
exclude_ports (list, tuple, set): List of ports that won't be
checked form entered range.
host (str): Host where will check for free ports. Set to
"localhost" by default.
"""
if port_from is None:
port_from = 8079
if port_to is None:
port_to = 65535
# Excluded ports (e.g. reserved for other servers/clients)
if exclude_ports is None:
exclude_ports = []
# Default host is localhost but it is possible to look for other hosts
if host is None:
host = "localhost"
found_port = None
for port in range(port_from, port_to + 1):
if port in exclude_ports:
continue
sock = None
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
found_port = port
except socket.error:
continue
finally:
if sock:
sock.close()
if found_port is not None:
break
return found_port

View file

@ -126,10 +126,6 @@
}
}
},
"rest_api": {
"default_port": 8021,
"exclude_ports": []
},
"timers_manager": {
"enabled": true,
"full_time": 15.0,

View file

@ -30,31 +30,6 @@
"type": "schema",
"name": "schema_ftrack"
},
{
"type": "dict",
"key": "rest_api",
"label": "Rest Api",
"collapsible": true,
"children": [
{
"type": "number",
"key": "default_port",
"label": "Default Port",
"minimum": 1,
"maximum": 65535
},
{
"type": "list",
"key": "exclude_ports",
"label": "Exclude ports",
"object_type": {
"type": "number",
"minimum": 1,
"maximum": 65535
}
}
]
},
{
"type": "dict",
"key": "timers_manager",
@ -217,4 +192,4 @@
]
}
]
}
}