diff --git a/aiida_restapi/config.py b/aiida_restapi/config.py index 9f877de..8702e2e 100644 --- a/aiida_restapi/config.py +++ b/aiida_restapi/config.py @@ -1,5 +1,7 @@ """Configuration of API""" +from importlib.metadata import version + # to get a string like this run: # openssl rand -hex 32 SECRET_KEY = '09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7' @@ -20,3 +22,8 @@ # The chunks size for streaming data for download DOWNLOAD_CHUNK_SIZE = 1024 + +API_CONFIG = { + 'PREFIX': version('aiida_restapi'), # prefix for all URLs + 'VERSION': '0.1.0a', +} diff --git a/aiida_restapi/main.py b/aiida_restapi/main.py index 5e75b8c..a47ae1a 100644 --- a/aiida_restapi/main.py +++ b/aiida_restapi/main.py @@ -1,9 +1,12 @@ """Declaration of FastAPI application.""" +from typing import Any + from fastapi import FastAPI +from aiida_restapi.config import API_CONFIG from aiida_restapi.graphql import main -from aiida_restapi.routers import auth, computers, daemon, groups, nodes, process, users +from aiida_restapi.routers import auth, computers, daemon, groups, nodes, process, server, users app = FastAPI() app.include_router(auth.router) @@ -14,3 +17,31 @@ app.include_router(users.router) app.include_router(process.router) app.add_route('/graphql', main.app, name='graphql') + + +# We need to create this endpoint here instead of in the server to avoid circular imports, since it is dependent on the +# app +@server.router.get('/server/endpoints', response_model=dict[str, Any]) +async def get_server_endpoints() -> dict[str, Any]: + """List available routes""" + import re + + from fastapi.routing import APIRoute + + routes = [] + for route in app.routes: + if isinstance(route, APIRoute): + full_path = API_CONFIG['PREFIX'] + route.path + match = re.search(r'^\/([^\/]+)', route.path) + endpoint_group = None if match is None else match.group(1) + routes.append( + { + 'path': full_path, + 'group': endpoint_group, + 'methods': route.methods, + } + ) + return {'endpoints': routes} + + +app.include_router(server.router) diff --git a/aiida_restapi/routers/server.py b/aiida_restapi/routers/server.py new file mode 100644 index 0000000..d1a4e56 --- /dev/null +++ b/aiida_restapi/routers/server.py @@ -0,0 +1,26 @@ +"""Declaration of FastAPI application.""" + +from typing import Any + +from aiida import __version__ as aiida_version +from fastapi import APIRouter + +from aiida_restapi.config import API_CONFIG + +router = APIRouter() + + +@router.get('/server/info', response_model=dict[str, Any]) +async def get_server_info() -> dict[str, Any]: + """Get the API version information""" + response = {} + api_version = API_CONFIG['VERSION'].split('.') + response['API_major_version'] = api_version[0] + response['API_minor_version'] = api_version[1] + response['API_revision_version'] = api_version[2] + # Add Rest API prefix + response['API_prefix'] = API_CONFIG['PREFIX'] + + # Add AiiDA version + response['AiiDA_version'] = aiida_version + return response diff --git a/tests/conftest.py b/tests/conftest.py index b680eba..93f5d53 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -317,32 +317,39 @@ def _func( return _func +def _create_node( + *, + label: str = '', + description: str = '', + attributes: Optional[dict] = None, + extras: Optional[dict] = None, + process_type: Optional[str] = None, + computer: Optional[orm.Computer] = None, + store: bool = True, +) -> orm.nodes.Node: + node = orm.CalcJobNode(computer=computer) if process_type else orm.Data(computer=computer) + node.label = label + node.description = description + node.base.attributes.reset(attributes or {}) + node.base.extras.reset(extras or {}) + if process_type is not None: + node.process_type = process_type + if store: + node.store() + return node + + @pytest.fixture def create_node(): """Create and store an AiiDA Node.""" + return _create_node - def _func( - *, - label: str = '', - description: str = '', - attributes: Optional[dict] = None, - extras: Optional[dict] = None, - process_type: Optional[str] = None, - computer: Optional[orm.Computer] = None, - store: bool = True, - ) -> orm.nodes.Node: - node = orm.CalcJobNode(computer=computer) if process_type else orm.Data(computer=computer) - node.label = label - node.description = description - node.base.attributes.reset(attributes or {}) - node.base.extras.reset(extras or {}) - if process_type is not None: - node.process_type = process_type - if store: - node.store() - return node - return _func +@pytest.fixture +def calcjob_nodes(): + """Populate database with calcjob nodes.""" + nodes = [_create_node(label=f'node {i}', process_type='', store=False) for i in range(4)] + return nodes @pytest.fixture