diff --git a/h5grove/fastapi_utils.py b/h5grove/fastapi_utils.py index e8a92b4..6271caf 100644 --- a/h5grove/fastapi_utils.py +++ b/h5grove/fastapi_utils.py @@ -16,6 +16,7 @@ __all__ = [ "router", "settings", + "get_root", "get_attr", "get_data", "get_meta", @@ -75,6 +76,12 @@ async def add_base_path(file): return f"{settings.base_dir}/{file}" if settings.base_dir else file +@router.api_route("/", methods=["GET", "HEAD"]) +async def get_root(): + """`/` endpoint handler to check server status""" + return Response("ok") + + @router.get("/attr/") async def get_attr( file: str = Depends(add_base_path), diff --git a/h5grove/flask_utils.py b/h5grove/flask_utils.py index 3c32afb..68d3ece 100644 --- a/h5grove/flask_utils.py +++ b/h5grove/flask_utils.py @@ -17,6 +17,7 @@ __all__ = [ + "root_route", "attr_route", "data_route", "meta_route", @@ -51,6 +52,11 @@ def create_error(status_code: int, message: str): ) +def root_route(): + """`/` endpoint handler to check server status""" + return "ok" + + def attr_route(): """`/attr/` endpoint handler""" filename = get_filename(request) @@ -110,6 +116,7 @@ def stats_route(): URL_RULES = { + "/": root_route, "/attr/": attr_route, "/data/": data_route, "/meta/": meta_route, diff --git a/h5grove/tornado_utils.py b/h5grove/tornado_utils.py index 14d59e9..7d026bc 100644 --- a/h5grove/tornado_utils.py +++ b/h5grove/tornado_utils.py @@ -16,6 +16,7 @@ from .utils import parse_bool_arg __all__ = [ + "RootHandler", "BaseHandler", "AttributeHandler", "DataHandler", @@ -69,6 +70,16 @@ def write_error(self, status_code: int, **kwargs: Any) -> None: self.finish({"message": self._reason}) +class RootHandler(BaseHandler): + """`/` endpoint handler to check server status""" + + def get(self): + self.finish("ok") + + def head(self): + self.finish() + + class ContentHandler(BaseHandler): def get_response( self, full_file_path: str, path: Optional[str], resolve_links: Optional[str] @@ -85,7 +96,7 @@ def get_content_response(self, content: EntityContent) -> Response: class AttributeHandler(ContentHandler): - """/attr/ endpoint handler""" + """`/attr/` endpoint handler""" def get_content_response(self, content: EntityContent) -> Response: assert isinstance(content, ResolvedEntityContent) @@ -96,7 +107,7 @@ def get_content_response(self, content: EntityContent) -> Response: class DataHandler(ContentHandler): - """/data/ endpoint handler""" + """`/data/` endpoint handler""" def get_content_response(self, content: EntityContent) -> Response: dtype = self.get_query_argument("dtype", None) @@ -112,14 +123,14 @@ def get_content_response(self, content: EntityContent) -> Response: class MetadataHandler(ContentHandler): - """/meta/ endpoint handler""" + """`/meta/` endpoint handler""" def get_content_response(self, content: EntityContent) -> Response: return encode(content.metadata()) class StatisticsHandler(ContentHandler): - """/stats/ endpoint handler""" + """`/stats/` endpoint handler""" def get_content_response(self, content: EntityContent) -> Response: selection = self.get_query_argument("selection", None) @@ -140,7 +151,7 @@ def get_response( # TODO: Setting the return type raises mypy errors def get_handlers(base_dir: Optional[str], allow_origin: Optional[str] = None): - """Build h5grove handlers (`/attr`, `/data`, `/meta` and `/stats`). + """Build h5grove handlers (`/`, `/attr/`, `/data/`, `/meta/` and `/stats/`). :param base_dir: Base directory from which the HDF5 files will be served :param allow_origin: Allowed origins for CORS @@ -148,6 +159,7 @@ def get_handlers(base_dir: Optional[str], allow_origin: Optional[str] = None): """ init_args = {"base_dir": base_dir, "allow_origin": allow_origin} return [ + (r"/", RootHandler, init_args), (r"/attr/.*", AttributeHandler, init_args), (r"/data/.*", DataHandler, init_args), (r"/meta/.*", MetadataHandler, init_args), diff --git a/test/base_test.py b/test/base_test.py index a027413..1a6c5d2 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -22,6 +22,11 @@ def server(self) -> Generator[BaseServer, None, None]: """Override in subclass with a fixture providing a :class:`BaseServer`""" raise NotImplementedError() + def test_ping(self, server): + """Test / endpoint used for checking server status""" + get_response = server.get("/") + assert get_response.content == b"ok" + def test_attr_on_root(self, server): """Test /attr/ endpoint on root group""" # Test condition