Skip to content

Commit

Permalink
Create endpoints for fetching workflows
Browse files Browse the repository at this point in the history
  • Loading branch information
csc-felipe committed Sep 28, 2022
1 parent 827f655 commit 1d3979f
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added [DPOP](https://oidcrp.readthedocs.io/en/latest/add_on/dpop.html) placeholder settings for when AAI support has been implemented
- Add advanced health checks for Datacite, REMS and Metax and performance checks for API response #585
- pre-commit check to sort and remove duplicates in the dictionary
- Add endpoint for fetching workflows #362

### Changed
- schema loader now matches schema files by exact match with schema #481. This means that schema file naming in metadata_backend/helpers/schemas now have rules:
Expand Down
56 changes: 56 additions & 0 deletions docs/specification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,62 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/403Forbidden"
/v1/workflows:
get:
tags:
- Query
summary: Available workflows
responses:
200:
description: OK
content:
application/json:
schema:
type: object
description: Workflow name as key, and description as value
401:
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/401Unauthorized"
/v1/workflows/{workflow}:
get:
tags:
- Query
summary: Retrieve a workflow definition by its name.
parameters:
- name: workflow
in: path
description: Name of the workflow
schema:
type: string
required: true
responses:
200:
description: OK
content:
application/json:
schema:
type: object
401:
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/401Unauthorized"
404:
description: Not Found
content:
application/json:
schema:
$ref: "#/components/schemas/404NotFound"
403:
description: Forbidden
content:
application/json:
schema:
$ref: "#/components/schemas/403Forbidden"
/v1/schemas:
get:
tags:
Expand Down
48 changes: 41 additions & 7 deletions metadata_backend/api/handlers/restapi.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""Handle HTTP methods for server."""
import json
from math import ceil
from typing import Dict, Tuple
from typing import Dict, List, Tuple, Union

import aiohttp_session
import ujson
from aiohttp import web
from aiohttp.web import Request, Response
from multidict import CIMultiDict

from ...conf.conf import schema_types
from ...conf.conf import WORKFLOWS, schema_types
from ...helpers.logger import LOG
from ...helpers.schema_loader import JSONSchemaLoader, SchemaNotFoundException
from ...services.datacite_service_handler import DataciteServiceHandler
Expand Down Expand Up @@ -128,16 +128,26 @@ async def _get_data(self, req: Request) -> Dict:
LOG.error(reason)
raise web.HTTPBadRequest(reason=reason)

@staticmethod
async def _json_response(data: Union[Dict, List[Dict]]) -> Response:
"""Reusable json response, serializing data with ujson.
:param data: Data to be serialized and made into HTTP 200 response
"""
return web.Response(
body=ujson.dumps(data, escape_forward_slashes=False), status=200, content_type="application/json"
)

async def get_schema_types(self, _: Request) -> Response:
"""Get all possible metadata schema types from database.
Basically returns which objects user can submit and query for.
:param _: GET Request
:returns: JSON list of schema types
"""
types_json = ujson.dumps([x["description"] for x in schema_types.values()], escape_forward_slashes=False)
data = [x["description"] for x in schema_types.values()]
LOG.info(f"GET schema types. Retrieved {len(schema_types)} schemas.")
return web.Response(body=types_json, status=200, content_type="application/json")
return await self._json_response(data)

async def get_json_schema(self, req: Request) -> Response:
"""Get all JSON Schema for a specific schema type.
Expand All @@ -157,15 +167,39 @@ async def get_json_schema(self, req: Request) -> Response:
else:
schema = JSONSchemaLoader().get_schema(schema_type)
LOG.info(f"{schema_type} schema loaded.")
return web.Response(
body=ujson.dumps(schema, escape_forward_slashes=False), status=200, content_type="application/json"
)
return await self._json_response(schema)

except SchemaNotFoundException as error:
reason = f"{error} ({schema_type})"
LOG.error(reason)
raise web.HTTPBadRequest(reason=reason)

async def get_workflows(self, _: Request) -> Response:
"""Get all JSON workflows.
Workflows tell what are the requirements for different 'types of submissions' (aka workflow)
:param _: GET Request
:returns: JSON list of workflows
"""
LOG.info(f"GET workflows. Retrieved {len(WORKFLOWS)} workflows.")
response = {workflow["name"]: workflow["description"] for workflow in WORKFLOWS.values()}
return await self._json_response(response)

async def get_workflow(self, req: Request) -> Response:
"""Get a single workflow definition by name.
:param req: GET Request
:raises: HTTPNotFound if workflow doesn't exist
:returns: workflow as a JSON object
"""
workflow_name = req.match_info["workflow"]
if workflow_name not in WORKFLOWS:
reason = f"Workflow {workflow_name} was not found."
LOG.exception(reason)
raise web.HTTPNotFound(reason=reason)
LOG.info(f"GET workflow {workflow_name}.")
return await self._json_response(WORKFLOWS[workflow_name])

def _header_links(self, url: str, page: int, size: int, total_objects: int) -> CIMultiDict[str]:
"""Create link header for pagination.
Expand Down
13 changes: 9 additions & 4 deletions metadata_backend/conf/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
Production version gets frontend SPA from this folder, after it has been built
and inserted here in projects Dockerfile.
"""

import os
from distutils.util import strtobool
from pathlib import Path
Expand Down Expand Up @@ -107,12 +106,18 @@ def create_db_client() -> AsyncIOMotorClient:
return AsyncIOMotorClient(url, connectTimeoutMS=connectTimeout, serverSelectionTimeoutMS=serverTimeout)


# 2) Load schema types and descriptions from json
# Default schemas will be ENA schemas
# 2) Load schema types, descriptions and workflows from json
path_to_schema_file = Path(__file__).parent / "schemas.json"
with open(path_to_schema_file, encoding="utf-8") as schema_file:
with open(path_to_schema_file, "rb") as schema_file:
schema_types = ujson.load(schema_file)

path_to_workflows = Path(__file__).parent / "workflows"
WORKFLOWS: Dict[str, Dict] = {}

for workflow_path in path_to_workflows.iterdir():
with open(workflow_path, "rb") as workflow_file:
workflow = ujson.load(workflow_file)
WORKFLOWS[workflow["name"]] = workflow

# 3) Define mapping between url query parameters and mongodb queries
query_map = {
Expand Down
9 changes: 6 additions & 3 deletions metadata_backend/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ async def close_http_clients(_: web.Application) -> None:

server.on_shutdown.append(close_http_clients)

_schema = RESTAPIHandler()
_common_api_handler = RESTAPIHandler()
_object = ObjectAPIHandler(
metax_handler=metax_handler, datacite_handler=datacite_handler, rems_handler=rems_handler
)
Expand All @@ -90,9 +90,12 @@ async def close_http_clients(_: web.Application) -> None:
)
_template = TemplatesAPIHandler()
api_routes = [
# retrieve workflows
web.get("/workflows", _common_api_handler.get_workflows),
web.get("/workflows/{workflow}", _common_api_handler.get_workflow),
# retrieve schema and information about it
web.get("/schemas", _schema.get_schema_types),
web.get("/schemas/{schema}", _schema.get_json_schema),
web.get("/schemas", _common_api_handler.get_schema_types),
web.get("/schemas/{schema}", _common_api_handler.get_json_schema),
# metadata objects operations
web.get("/objects/{schema}", _object.query_objects),
web.post("/objects/{schema}", _object.post_object),
Expand Down
1 change: 1 addition & 0 deletions tests/integration/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
submit_url = f"{base_url}{API_PREFIX}/submit"
publish_url = f"{base_url}{API_PREFIX}/publish"
schemas_url = f"{base_url}{API_PREFIX}/schemas"
workflows_url = f"{base_url}{API_PREFIX}/workflows"
metax_url = f"{os.getenv('METAX_URL', 'http://localhost:8002')}"
metax_api = f"{metax_url}/rest/v2/datasets"
datacite_url = f"{os.getenv('DOI_API', 'http://localhost:8001')}"
Expand Down
20 changes: 16 additions & 4 deletions tests/integration/test_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Test other endpoints."""
import logging

from tests.integration.conf import base_url, schemas_url, test_schemas
from tests.integration.conf import base_url, schemas_url, test_schemas, workflows_url

LOG = logging.getLogger(__name__)
LOG.setLevel(logging.DEBUG)


class TestHealthEndpoint:
class TestPublicEndpoints:
"""Test health endpoint."""

async def test_health_check(self, client):
Expand All @@ -23,11 +23,23 @@ async def test_health_check(self, client):
assert res["services"]["database"]["status"] == "Ok"


class TestSchemasEndpoint:
"""Test schemas endpoint."""
class TestOtherApiEndpoints:
"""Test other api endpoints."""

async def test_schemas_endpoint(self, client_logged_in):
"""Test that schemas' endpoint return 200."""
for schema, expected_status in test_schemas:
async with client_logged_in.get(f"{schemas_url}/{schema}") as resp:
assert resp.status == expected_status, f"{resp.status} {schema}"

async def test_workflows_endpoint(self, client_logged_in):
"""Test that schemas' endpoint return 200."""
async with client_logged_in.get(f"{workflows_url}") as resp:
assert resp.status == 200, resp.status
workflows = await resp.json()

for workflow_name in workflows.keys():
async with client_logged_in.get(f"{workflows_url}/{workflow_name}") as resp:
assert resp.status == 200, resp.status
workflow = await resp.json()
assert workflow_name == workflow["name"]
4 changes: 2 additions & 2 deletions tests/unit/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ async def test_api_routes_are_set(self):
- /submissions/{submissionId}/doi
NOTE: If there's swagger or frontend folder generated in metadata_backend
tests will see 22 or 23 routes
tests will see more routes
"""
server = await self.get_application()
self.assertIs(len(server.router.routes()), 47)
self.assertIs(len(server.router.routes()), 51)

async def test_frontend_routes_are_set(self):
"""Test correct routes are set when frontend folder exists."""
Expand Down

0 comments on commit 1d3979f

Please sign in to comment.