ray-project--ray
267 行
9.5 KiB
Python
267 行
9.5 KiB
Python
import dataclasses
|
|
from dataclasses import asdict, fields
|
|
from typing import Awaitable, Callable, List, Tuple
|
|
|
|
import aiohttp.web
|
|
|
|
from ray.dashboard.optional_utils import rest_response
|
|
from ray.dashboard.utils import HTTPStatusCode
|
|
from ray.util.state.common import (
|
|
DEFAULT_LIMIT,
|
|
DEFAULT_RPC_TIMEOUT,
|
|
RAY_MAX_LIMIT_FROM_API_SERVER,
|
|
ListApiOptions,
|
|
ListApiResponse,
|
|
PredicateType,
|
|
StateSchema,
|
|
SummaryApiOptions,
|
|
SummaryApiResponse,
|
|
SupportedFilterType,
|
|
filter_fields,
|
|
)
|
|
from ray.util.state.exception import DataSourceUnavailable
|
|
from ray.util.state.util import convert_string_to_type
|
|
|
|
|
|
def do_reply(
|
|
status_code: HTTPStatusCode, error_message: str, result: ListApiResponse, **kwargs
|
|
):
|
|
return rest_response(
|
|
status_code=status_code,
|
|
message=error_message,
|
|
result=result,
|
|
convert_google_style=False,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
async def handle_list_api(
|
|
list_api_fn: Callable[[ListApiOptions], Awaitable[ListApiResponse]],
|
|
req: aiohttp.web.Request,
|
|
):
|
|
try:
|
|
result = await list_api_fn(option=options_from_req(req))
|
|
return do_reply(
|
|
status_code=HTTPStatusCode.OK,
|
|
error_message="",
|
|
result=asdict(result),
|
|
)
|
|
except ValueError as e:
|
|
return do_reply(
|
|
status_code=HTTPStatusCode.BAD_REQUEST,
|
|
error_message=str(e),
|
|
result=None,
|
|
)
|
|
except DataSourceUnavailable as e:
|
|
return do_reply(
|
|
status_code=HTTPStatusCode.INTERNAL_ERROR,
|
|
error_message=str(e),
|
|
result=None,
|
|
)
|
|
|
|
|
|
def _get_filters_from_req(
|
|
req: aiohttp.web.Request,
|
|
) -> List[Tuple[str, PredicateType, SupportedFilterType]]:
|
|
filter_keys = req.query.getall("filter_keys", [])
|
|
filter_predicates = req.query.getall("filter_predicates", [])
|
|
filter_values = req.query.getall("filter_values", [])
|
|
assert len(filter_keys) == len(filter_values)
|
|
filters = []
|
|
for key, predicate, val in zip(filter_keys, filter_predicates, filter_values):
|
|
filters.append((key, predicate, val))
|
|
return filters
|
|
|
|
|
|
def options_from_req(req: aiohttp.web.Request) -> ListApiOptions:
|
|
"""Obtain `ListApiOptions` from the aiohttp request."""
|
|
limit = int(
|
|
req.query.get("limit") if req.query.get("limit") is not None else DEFAULT_LIMIT
|
|
)
|
|
|
|
if limit > RAY_MAX_LIMIT_FROM_API_SERVER:
|
|
raise ValueError(
|
|
f"Given limit {limit} exceeds the supported "
|
|
f"limit {RAY_MAX_LIMIT_FROM_API_SERVER}. Use a lower limit, or set the "
|
|
f"`RAY_MAX_LIMIT_FROM_API_SERVER` environment variable to a larger value."
|
|
)
|
|
|
|
timeout = int(req.query.get("timeout", 30))
|
|
filters = _get_filters_from_req(req)
|
|
detail = convert_string_to_type(req.query.get("detail", False), bool)
|
|
exclude_driver = convert_string_to_type(req.query.get("exclude_driver", True), bool)
|
|
|
|
return ListApiOptions(
|
|
limit=limit,
|
|
timeout=timeout,
|
|
filters=filters,
|
|
detail=detail,
|
|
exclude_driver=exclude_driver,
|
|
)
|
|
|
|
|
|
def summary_options_from_req(req: aiohttp.web.Request) -> SummaryApiOptions:
|
|
timeout = int(req.query.get("timeout", DEFAULT_RPC_TIMEOUT))
|
|
filters = _get_filters_from_req(req)
|
|
summary_by = req.query.get("summary_by", None)
|
|
return SummaryApiOptions(timeout=timeout, filters=filters, summary_by=summary_by)
|
|
|
|
|
|
async def handle_summary_api(
|
|
summary_fn: Callable[[SummaryApiOptions], SummaryApiResponse],
|
|
req: aiohttp.web.Request,
|
|
):
|
|
result = await summary_fn(option=summary_options_from_req(req))
|
|
return do_reply(
|
|
status_code=HTTPStatusCode.OK,
|
|
error_message="",
|
|
result=asdict(result),
|
|
)
|
|
|
|
|
|
def convert_filters_type(
|
|
filter: List[Tuple[str, PredicateType, SupportedFilterType]],
|
|
schema: StateSchema,
|
|
) -> List[Tuple[str, PredicateType, SupportedFilterType]]:
|
|
"""Convert the given filter's type to SupportedFilterType.
|
|
|
|
This method is necessary because click can only accept a single type
|
|
for its tuple (which is string in this case).
|
|
|
|
Args:
|
|
filter: A list of filter which is a tuple of (key, val).
|
|
schema: The state schema. It is used to infer the type of the column for filter.
|
|
|
|
Returns:
|
|
A new list of filters with correct types that match the schema.
|
|
"""
|
|
new_filter = []
|
|
if dataclasses.is_dataclass(schema):
|
|
schema = {field.name: field.type for field in fields(schema)}
|
|
else:
|
|
schema = schema.schema_dict()
|
|
|
|
for col, predicate, val in filter:
|
|
if col in schema:
|
|
column_type = schema[col]
|
|
try:
|
|
isinstance(val, column_type)
|
|
except TypeError:
|
|
# Calling `isinstance` to the Literal type raises a TypeError.
|
|
# Ignore this case.
|
|
pass
|
|
else:
|
|
if isinstance(val, column_type):
|
|
# Do nothing.
|
|
pass
|
|
elif column_type is int or column_type == "integer":
|
|
try:
|
|
val = convert_string_to_type(val, int)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Invalid filter `--filter {col} {val}` for a int type "
|
|
"column. Please provide an integer filter "
|
|
f"`--filter {col} [int]`"
|
|
)
|
|
elif column_type is float or column_type == "number":
|
|
try:
|
|
val = convert_string_to_type(
|
|
val,
|
|
float,
|
|
)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Invalid filter `--filter {col} {val}` for a float "
|
|
"type column. Please provide an integer filter "
|
|
f"`--filter {col} [float]`"
|
|
)
|
|
elif column_type is bool or column_type == "boolean":
|
|
try:
|
|
val = convert_string_to_type(val, bool)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Invalid filter `--filter {col} {val}` for a boolean "
|
|
"type column. Please provide "
|
|
f"`--filter {col} [True|true|1]` for True or "
|
|
f"`--filter {col} [False|false|0]` for False."
|
|
)
|
|
new_filter.append((col, predicate, val))
|
|
return new_filter
|
|
|
|
|
|
def do_filter(
|
|
data: List[dict],
|
|
filters: List[Tuple[str, PredicateType, SupportedFilterType]],
|
|
state_dataclass: StateSchema,
|
|
detail: bool,
|
|
) -> List[dict]:
|
|
"""Return the filtered data given filters.
|
|
|
|
Args:
|
|
data: A list of state data.
|
|
filters: A list of KV tuple to filter data (key, val). The data is filtered
|
|
if data[key] != val.
|
|
state_dataclass: The state schema.
|
|
detail: If True, include detail-only columns; otherwise drop them.
|
|
|
|
Returns:
|
|
A list of filtered state data in dictionary. Each state data's
|
|
unnecessary columns are filtered by the given state_dataclass schema.
|
|
"""
|
|
filters = convert_filters_type(filters, state_dataclass)
|
|
result = []
|
|
for datum in data:
|
|
match = True
|
|
for filter_column, filter_predicate, filter_value in filters:
|
|
filterable_columns = state_dataclass.filterable_columns()
|
|
filter_column = filter_column.lower()
|
|
if filter_column not in filterable_columns:
|
|
raise ValueError(
|
|
f"The given filter column {filter_column} is not supported. "
|
|
"Enter filters with –-filter key=value "
|
|
"or –-filter key!=value "
|
|
f"Supported filter columns: {filterable_columns}"
|
|
)
|
|
|
|
if filter_column not in datum:
|
|
match = False
|
|
elif filter_predicate == "=":
|
|
if isinstance(filter_value, str) and isinstance(
|
|
datum[filter_column], str
|
|
):
|
|
# Case insensitive match for string filter values.
|
|
match = datum[filter_column].lower() == filter_value.lower()
|
|
elif isinstance(filter_value, str) and isinstance(
|
|
datum[filter_column], bool
|
|
):
|
|
match = datum[filter_column] == convert_string_to_type(
|
|
filter_value, bool
|
|
)
|
|
elif isinstance(filter_value, str) and isinstance(
|
|
datum[filter_column], int
|
|
):
|
|
match = datum[filter_column] == convert_string_to_type(
|
|
filter_value, int
|
|
)
|
|
else:
|
|
match = datum[filter_column] == filter_value
|
|
elif filter_predicate == "!=":
|
|
if isinstance(filter_value, str) and isinstance(
|
|
datum[filter_column], str
|
|
):
|
|
match = datum[filter_column].lower() != filter_value.lower()
|
|
else:
|
|
match = datum[filter_column] != filter_value
|
|
else:
|
|
raise ValueError(
|
|
f"Unsupported filter predicate {filter_predicate} is given. "
|
|
"Available predicates: =, !=."
|
|
)
|
|
|
|
if not match:
|
|
break
|
|
|
|
if match:
|
|
result.append(filter_fields(datum, state_dataclass, detail))
|
|
return result
|