214 lines
6.6 KiB
Python
214 lines
6.6 KiB
Python
import abc
|
|
import collections
|
|
import functools
|
|
import inspect
|
|
import json
|
|
import logging
|
|
import os
|
|
import traceback
|
|
from typing import Any
|
|
|
|
from ray.dashboard.optional_deps import PathLike, RouteDef, aiohttp, hdrs
|
|
from ray.dashboard.utils import CustomEncoder, HTTPStatusCode, to_google_style
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BaseRouteTable(abc.ABC):
|
|
"""A base class to bind http route to a target instance. Subclass should implement
|
|
the _register_route method. It should define how the handler interacts with
|
|
_BindInfo.instance.
|
|
|
|
Subclasses must declare their own _bind_map and _routes properties to avoid
|
|
conflicts.
|
|
"""
|
|
|
|
class _BindInfo:
|
|
def __init__(self, filename, lineno, instance):
|
|
self.filename = filename
|
|
self.lineno = lineno
|
|
self.instance = instance
|
|
|
|
@classmethod
|
|
@property
|
|
@abc.abstractmethod
|
|
def _bind_map(cls):
|
|
pass
|
|
|
|
@classmethod
|
|
@property
|
|
@abc.abstractmethod
|
|
def _routes(cls):
|
|
pass
|
|
|
|
@classmethod
|
|
@abc.abstractmethod
|
|
def _register_route(cls, method, path, **kwargs):
|
|
pass
|
|
|
|
@classmethod
|
|
@abc.abstractmethod
|
|
def bind(cls, instance):
|
|
pass
|
|
|
|
@classmethod
|
|
def routes(cls):
|
|
return cls._routes
|
|
|
|
@classmethod
|
|
def bound_routes(cls):
|
|
bound_items = []
|
|
for r in cls._routes._items:
|
|
if isinstance(r, RouteDef):
|
|
route_method = r.handler.__route_method__
|
|
route_path = r.handler.__route_path__
|
|
instance = cls._bind_map[route_method][route_path].instance
|
|
if instance is not None:
|
|
bound_items.append(r)
|
|
else:
|
|
bound_items.append(r)
|
|
routes = aiohttp.web.RouteTableDef()
|
|
routes._items = bound_items
|
|
return routes
|
|
|
|
@classmethod
|
|
def head(cls, path, **kwargs):
|
|
return cls._register_route(hdrs.METH_HEAD, path, **kwargs)
|
|
|
|
@classmethod
|
|
def get(cls, path, **kwargs):
|
|
return cls._register_route(hdrs.METH_GET, path, **kwargs)
|
|
|
|
@classmethod
|
|
def post(cls, path, **kwargs):
|
|
return cls._register_route(hdrs.METH_POST, path, **kwargs)
|
|
|
|
@classmethod
|
|
def put(cls, path, **kwargs):
|
|
return cls._register_route(hdrs.METH_PUT, path, **kwargs)
|
|
|
|
@classmethod
|
|
def patch(cls, path, **kwargs):
|
|
return cls._register_route(hdrs.METH_PATCH, path, **kwargs)
|
|
|
|
@classmethod
|
|
def delete(cls, path, **kwargs):
|
|
return cls._register_route(hdrs.METH_DELETE, path, **kwargs)
|
|
|
|
@classmethod
|
|
def view(cls, path, **kwargs):
|
|
return cls._register_route(hdrs.METH_ANY, path, **kwargs)
|
|
|
|
@classmethod
|
|
def static(cls, prefix: str, path: PathLike, **kwargs: Any) -> None:
|
|
cls._routes.static(prefix, path, **kwargs)
|
|
|
|
|
|
def method_route_table_factory():
|
|
"""
|
|
Return a method-based route table class, for in-process HeadModule objects.
|
|
"""
|
|
|
|
class MethodRouteTable(BaseRouteTable):
|
|
"""A helper class to bind http route to class method. Each _BindInfo.instance
|
|
is a class instance, and for an inbound request, we invoke the async handler
|
|
method."""
|
|
|
|
_bind_map = collections.defaultdict(dict)
|
|
_routes = aiohttp.web.RouteTableDef()
|
|
|
|
@classmethod
|
|
def _register_route(cls, method, path, **kwargs):
|
|
def _wrapper(handler):
|
|
if path in cls._bind_map[method]:
|
|
bind_info = cls._bind_map[method][path]
|
|
raise Exception(
|
|
f"Duplicated route path: {path}, "
|
|
f"previous one registered at "
|
|
f"{bind_info.filename}:{bind_info.lineno}"
|
|
)
|
|
|
|
bind_info = cls._BindInfo(
|
|
handler.__code__.co_filename, handler.__code__.co_firstlineno, None
|
|
)
|
|
|
|
@functools.wraps(handler)
|
|
async def _handler_route(*args) -> aiohttp.web.Response:
|
|
try:
|
|
# Make the route handler as a bound method.
|
|
# The args may be:
|
|
# * (Request, )
|
|
# * (self, Request)
|
|
req = args[-1]
|
|
return await handler(bind_info.instance, req)
|
|
except Exception:
|
|
logger.exception("Handle %s %s failed.", method, path)
|
|
return rest_response(
|
|
status_code=HTTPStatusCode.INTERNAL_ERROR,
|
|
message=traceback.format_exc(),
|
|
)
|
|
|
|
cls._bind_map[method][path] = bind_info
|
|
_handler_route.__route_method__ = method
|
|
_handler_route.__route_path__ = path
|
|
return cls._routes.route(method, path, **kwargs)(_handler_route)
|
|
|
|
return _wrapper
|
|
|
|
@classmethod
|
|
def bind(cls, instance):
|
|
def predicate(o):
|
|
if inspect.ismethod(o):
|
|
return hasattr(o, "__route_method__") and hasattr(
|
|
o, "__route_path__"
|
|
)
|
|
return False
|
|
|
|
handler_routes = inspect.getmembers(instance, predicate)
|
|
for _, h in handler_routes:
|
|
cls._bind_map[h.__func__.__route_method__][
|
|
h.__func__.__route_path__
|
|
].instance = instance
|
|
|
|
return MethodRouteTable
|
|
|
|
|
|
def rest_response(
|
|
status_code: HTTPStatusCode,
|
|
message: str,
|
|
convert_google_style: bool = True,
|
|
**kwargs,
|
|
) -> aiohttp.web.Response:
|
|
"""
|
|
Args:
|
|
status_code: HTTPStatusCode
|
|
The HTTP status code of the response.
|
|
message: str
|
|
The message of the response.
|
|
convert_google_style: bool
|
|
Whether to convert the response to google style.
|
|
**kwargs: Additional fields included in the response ``data`` payload.
|
|
|
|
Returns:
|
|
aiohttp.web.Response
|
|
"""
|
|
# In the dev context we allow a dev server running on a
|
|
# different port to consume the API, meaning we need to allow
|
|
# cross-origin access
|
|
if os.environ.get("RAY_DASHBOARD_DEV") == "1":
|
|
headers = {"Access-Control-Allow-Origin": "*"}
|
|
else:
|
|
headers = {}
|
|
|
|
success = status_code == HTTPStatusCode.OK
|
|
return aiohttp.web.json_response(
|
|
{
|
|
"result": success,
|
|
"msg": message,
|
|
"data": to_google_style(kwargs) if convert_google_style else kwargs,
|
|
},
|
|
dumps=functools.partial(json.dumps, cls=CustomEncoder),
|
|
headers=headers,
|
|
status=status_code,
|
|
)
|