Files
2026-07-13 13:17:40 +08:00

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