From 4fb8605d06f1ff9b52e163ec6d6411043de36be1 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:11:12 +0100 Subject: [PATCH 01/14] validate_metadata api --- app/ro_crates/routes/post_routes.py | 40 ++++++++++++++++- app/services/validation_service.py | 34 +++++++++++++++ app/tasks/validation_tasks.py | 67 ++++++++++++++++++++++++++++- app/utils/file_utils.py | 54 +++++++++++++++++++++++ 4 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 app/utils/file_utils.py diff --git a/app/ro_crates/routes/post_routes.py b/app/ro_crates/routes/post_routes.py index 12f849b..f73326e 100644 --- a/app/ro_crates/routes/post_routes.py +++ b/app/ro_crates/routes/post_routes.py @@ -8,7 +8,10 @@ from apiflask.fields import Integer, String from flask import request, Response -from app.services.validation_service import queue_ro_crate_validation_task +from app.services.validation_service import ( + queue_ro_crate_validation_task, + queue_ro_crate_metadata_validation_task +) post_routes_bp = APIBlueprint("post_routes", __name__) @@ -17,6 +20,10 @@ class validate_data(Schema): profile_name = String(required=False) webhook_url = String(required=False) +class validate_json(Schema): + crate_json = String(required=True) + profile_name = String(required=False) + @post_routes_bp.post("/validate_by_id") @post_routes_bp.input(validate_data(partial=True), location='json') @@ -80,3 +87,34 @@ def validate_ro_crate_from_id_no_webhook(json_data) -> tuple[Response, int]: profile_name = None return queue_ro_crate_validation_task(crate_id, profile_name) + + +@post_routes_bp.post("/validate_metadata") +@post_routes_bp.input(validate_json(partial=True), location='json') # -> json_data +def validate_ro_crate_metadata(json_data) -> tuple[Response, int]: + """ + Endpoint to validate an RO-Crate JSON file uploaded to the Service. + + Parameters: + - **crate_json**: The RO-Crate JSON-LD, as a string. _Required_ + - **profile_name**: The profile name for validation. _Optional_. + + Returns: + - A tuple containing the validation task's response and an HTTP status code. + + Raises: + - KeyError: If required parameters (`crate_json`) are missing. + """ + + try: + crate_json = json_data['crate_json'] + except: + raise KeyError("Missing required parameter: 'crate_json'") + + try: + profile_name = json_data['profile_name'] + except: + profile_name = None + + return queue_ro_crate_metadata_validation_task(crate_json, profile_name) + diff --git a/app/services/validation_service.py b/app/services/validation_service.py index 669ab38..357b10e 100644 --- a/app/services/validation_service.py +++ b/app/services/validation_service.py @@ -10,6 +10,7 @@ from app.tasks.validation_tasks import ( process_validation_task_by_id, + process_validation_task_by_metadata, return_ro_crate_validation ) @@ -42,6 +43,39 @@ def queue_ro_crate_validation_task( return jsonify({"error": str(e)}), 500 +def queue_ro_crate_metadata_validation_task( + crate_json, profile_name=None, webhook_url=None +) -> tuple[Response, int]: + """ + Queues an RO-Crate for validation with Celery. + + :param crate_id: The ID of the RO-Crate to validate. + :param profile_name: The profile to validate against. + :param webhook_url: The URL to POST the validation results to. + :return: A tuple containing a JSON response and an HTTP status code. + :raises: Exception: If an error occurs whilst queueing the task. + """ + + logging.info(f"Processing: {crate_json}, {profile_name}, {webhook_url}") + + if not crate_json: + return jsonify({"error": "Missing required parameter: crate_json"}), 400 + + try: + result = process_validation_task_by_metadata.delay( + crate_json, + profile_name, + webhook_url + ) + if webhook_url: + return jsonify({"message": "Validation in progress"}), 202 + else: + return jsonify({"result": result.get()}), 200 + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + def get_ro_crate_validation_task( crate_id ) -> tuple[Response, int]: diff --git a/app/tasks/validation_tasks.py b/app/tasks/validation_tasks.py index c58799e..261aa67 100644 --- a/app/tasks/validation_tasks.py +++ b/app/tasks/validation_tasks.py @@ -6,6 +6,8 @@ import logging import os +import shutil +from typing import Optional from rocrate_validator import services from rocrate_validator.models import ValidationResult @@ -17,6 +19,7 @@ get_validation_status_from_minio ) from app.utils.webhook_utils import send_webhook_notification +from app.utils.file_utils import build_metadata_only_rocrate logger = logging.getLogger(__name__) @@ -78,8 +81,67 @@ def process_validation_task_by_id( os.remove(file_path) +@celery.task +def process_validation_task_by_metadata( + crate_json: str, profile_name: str | None, webhook_url: str | None +) -> ValidationResult | str: + """ + Background task to process the RO-Crate validation for a given json metadata string. + + :param crate_json: A string containing the RO-Crate JSON metadata to validate. + :param profile_name: The name of the validation profile to use. Defaults to None. + :param webhook_url: The webhook URL to send notifications to. Defaults to None. + :raises Exception: If an error occurs during the validation process. + + :todo: Replace the Crate ID with a more comprehensive system, and replace profile name with URI. + """ + + skip_checks_list = ['ro-crate-1.1_12.1'] + file_path = None + + try: + # Fetch the RO-Crate from MinIO using the provided ID: + file_path = build_metadata_only_rocrate(crate_json) + + logging.info(f"Processing validation task for {file_path}") + + # Perform validation: + validation_result = perform_ro_crate_validation(file_path, + profile_name, + skip_checks_list + ) + + if isinstance(validation_result, str): + logging.error(f"Validation failed: {validation_result}") + # TODO: Send webhook with failure notification + raise Exception(f"Validation failed: {validation_result}") + + if not validation_result.has_issues(): + logging.info(f"RO Crate {file_path} is valid.") + else: + logging.info(f"RO Crate {file_path} is invalid.") + + if webhook_url: + send_webhook_notification(webhook_url, validation_result.to_json()) + + except Exception as e: + logging.error(f"Error processing validation task: {e}") + + # Send failure notification via webhook + error_data = {"profile_name": profile_name, "error": str(e)} + send_webhook_notification(webhook_url, error_data) + + finally: + # Clean up the temporary file if it was created: + if file_path and os.path.exists(file_path): + shutil.rmtree(file_path) + + return validation_result.to_json() + + + def perform_ro_crate_validation( - file_path: str, profile_name: str | None + file_path: str, profile_name: str | None, skip_checks_list: Optional[list] = None ) -> ValidationResult | str: """ Validates an RO-Crate using the provided file path and profile name. @@ -87,6 +149,7 @@ def perform_ro_crate_validation( :param file_path: The path to the RO-Crate file to validate :param profile_name: The name of the validation profile to use. Defaults to None. If None, the CRS4 validator will attempt to determine the profile. + :param skip_checks_list: A list of checks to skip, if needed :return: The validation result. :raises Exception: If an error occurs during the validation process. """ @@ -102,8 +165,8 @@ def perform_ro_crate_validation( ) settings = services.ValidationSettings( rocrate_uri=full_file_path, - # Only include profile_identifier if the profile_name is provided: **({"profile_identifier": profile_name} if profile_name else {}), + **({"skip_checks": skip_checks_list} if skip_checks_list else {}) ) return services.validate(settings) diff --git a/app/utils/file_utils.py b/app/utils/file_utils.py new file mode 100644 index 0000000..0b20eac --- /dev/null +++ b/app/utils/file_utils.py @@ -0,0 +1,54 @@ +"""Utility methods for interacting with the File System.""" + +# Author: Douglas Lowe, Alexander Hambley +# License: MIT +# Copyright (c) 2025 eScience Lab, The University of Manchester + +import json +import logging +import os +import tempfile + +from dotenv import load_dotenv + + +logger = logging.getLogger(__name__) + + +def build_metadata_only_rocrate(crate_json: str) -> str: + """ + Creates a temporary directory for an empty RO-Crate, + and saves the JSON string as a metadata file. + + :param crate_json: The metadata string. + :return: The local file path where the RO-Crate is saved. + :raises ValueError: If the required environment variables are not set. + :raises Exception: If an unexpected error occurs during the operation. + """ + + load_dotenv() + + try: + # Prepare temporary file path to store RO Crate for validation: + temp_dir = tempfile.mkdtemp() + file_path = os.path.join(temp_dir, 'ro-crate-metadata.json') + + logging.info( + f"Creating RO-Crate Metadata file. File path: {file_path}" + ) + with open(file_path, 'w') as f: + f.write(crate_json) + logging.info( + f"RO-Crate metadata successfully saved to {file_path}." + ) + + return temp_dir + + except ValueError as value_error: + logging.error(f"Configuration Error: {value_error}") + raise + + except Exception as e: + logging.error(f"Unexpected error creating RO-Crate metadata: {e}") + raise + From 5dfecbe4438b58fcbca383103d29ffcf64696e42 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:35:36 +0100 Subject: [PATCH 02/14] metadata api integration test --- requirements.testing.txt | 10 +++ tests/data/ro-crate-metadata.json | 101 ++++++++++++++++++++++++++++++ tests/metadata_integration.py | 53 ++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 requirements.testing.txt create mode 100644 tests/data/ro-crate-metadata.json create mode 100644 tests/metadata_integration.py diff --git a/requirements.testing.txt b/requirements.testing.txt new file mode 100644 index 0000000..6bec2e6 --- /dev/null +++ b/requirements.testing.txt @@ -0,0 +1,10 @@ +celery~=5.4.0 +minio~=7.2.10 +requests~=2.32.3 +Flask~=3.0.3 +Werkzeug~=3.0.4 +redis~=5.2.0 +python-dotenv~=1.0.1 +apiflask~=2.4.0 +roc-validator~=0.6.5 +pytest~=8.4.1 diff --git a/tests/data/ro-crate-metadata.json b/tests/data/ro-crate-metadata.json new file mode 100644 index 0000000..957ddea --- /dev/null +++ b/tests/data/ro-crate-metadata.json @@ -0,0 +1,101 @@ +{ + "@context": "https://w3id.org/ro/crate/1.1/context", + "@graph": [ + { + "@id": "./", + "@type": "Dataset", + "name": "example dataset", + "description": "crate that should conform with validator requirements", + "datePublished": "2024-03-17T15:55:54+00:00", + "license": { + "@id": "http://spdx.org/licenses/MIT" + }, + "hasPart": [ + { + "@id": "paper.pdf" + }, + { + "@id": "results.csv" + }, + { + "@id": "images/figure.svg" + }, + { + "@id": "logs/" + } + ] + }, + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "about": { + "@id": "./" + }, + "conformsTo": { + "@id": "https://w3id.org/ro/crate/1.1" + } + }, + { + "@id": "paper.pdf", + "@type": "File", + "author": [ + { + "@id": "https://orcid.org/0000-0000-0000-0000" + }, + { + "@id": "https://orcid.org/0000-0000-0000-0001" + } + ], + "encodingFormat": "application/pdf", + "name": "manuscript" + }, + { + "@id": "results.csv", + "@type": "File", + "author": { + "@id": "https://orcid.org/0000-0000-0000-0000" + }, + "encodingFormat": "text/csv", + "name": "experimental data" + }, + { + "@id": "images/figure.svg", + "@type": "File", + "author": { + "@id": "https://orcid.org/0000-0000-0000-0001" + }, + "encodingFormat": "image/svg+xml", + "name": "bar chart" + }, + { + "@id": "logs/", + "@type": "Dataset" + }, + { + "@id": "https://orcid.org/0000-0000-0000-0000", + "@type": "Person", + "affiliation": "University of Flatland", + "name": "Alice Doe" + }, + { + "@id": "https://orcid.org/0000-0000-0000-0001", + "@type": "Person", + "affiliation": "University of Flatland", + "name": "Bob Doe" + }, + { + "@id": "#hdr2024", + "@type": "Project Meeting", + "endDate": "2024-04-16", + "location": "Swansea, UK", + "name": "HDR-UK 2024", + "startDate": "2024-04-15" + }, + { + "@id": "https://spdx.org/licenses/MIT", + "@type": "CreativeWork", + "name": "MIT Licence", + "identifier": "MIT" + } + ] +} \ No newline at end of file diff --git a/tests/metadata_integration.py b/tests/metadata_integration.py new file mode 100644 index 0000000..2196ddd --- /dev/null +++ b/tests/metadata_integration.py @@ -0,0 +1,53 @@ +import pytest +import subprocess +import time +import requests +import json +import os + + +@pytest.fixture(scope="session", autouse=True) +def docker_compose(): + """Start Docker Compose before tests, shut down after.""" + print("Starting Docker Compose...") + subprocess.run( + ["docker", "compose", "-f", "docker-compose-develop.yml", "up", "-d"], + check=True + ) + time.sleep(10) # Wait for services to start — adjust as needed + + yield # Run the tests + + print("Stopping Docker Compose...") + subprocess.run(["docker", "compose", "down"], check=True) + + + +def test_validate_metadata(): + url = "http://localhost:5001/ro_crates/validate_metadata" + headers = { + "accept": "application/json", + "Content-Type": "application/json" + } + + # Load the JSON from file + filepath = os.path.join("tests/data", "ro-crate-metadata.json") + with open(filepath, "r", encoding="utf-8") as f: + crate_json_data = json.load(f) + + # The API expects the JSON to be passed as a string + payload = { + "crate_json": json.dumps(crate_json_data) + } + + response = requests.post(url, json=payload, headers=headers) + + response_result = json.loads(response.json()['result']) + + # Print response for debugging + print("Status Code:", response.status_code) + print("Response JSON:", response_result) + + # Assertions — update based on expected API behavior + assert response.status_code == 200 + assert response_result['passed'] == True \ No newline at end of file From 7a0c76a727bee979956567d9b128de2ac9966e03 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:44:55 +0100 Subject: [PATCH 03/14] gh action for integration test --- .github/workflows/test_docker.yml | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/test_docker.yml diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml new file mode 100644 index 0000000..db06678 --- /dev/null +++ b/.github/workflows/test_docker.yml @@ -0,0 +1,39 @@ +name: Integration Tests + +on: + pull_request: + branches: [ develop ] + +jobs: + test: + runs-on: ubuntu-latest + + services: + docker: + image: docker:24.0.6 + options: --privileged + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest requests + + - name: Build Docker Compose Containers + run: | + docker compose -f docker-compose-develop.yml build + + - name: Spin Up Docker Compose and Run Tests + run: pytest tests/metadata_integration.py -v + + - name: Ensure that Docker Compose is Shutdown + if: always() + run: docker compose down From 4f1452abf55032fb46701e7252804dbb4bb6359c Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:49:27 +0100 Subject: [PATCH 04/14] load example env for tests --- .github/workflows/test_docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index db06678..d9f5ba6 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -29,6 +29,7 @@ jobs: - name: Build Docker Compose Containers run: | + cp example.env .env docker compose -f docker-compose-develop.yml build - name: Spin Up Docker Compose and Run Tests From 24a558860f4012003b66fd2cd356420c93083038 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:05:25 +0100 Subject: [PATCH 05/14] PEP8 conformance for validation classes --- app/ro_crates/routes/post_routes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/ro_crates/routes/post_routes.py b/app/ro_crates/routes/post_routes.py index f73326e..2b98d1a 100644 --- a/app/ro_crates/routes/post_routes.py +++ b/app/ro_crates/routes/post_routes.py @@ -15,18 +15,18 @@ post_routes_bp = APIBlueprint("post_routes", __name__) -class validate_data(Schema): +class ValidateData(Schema): crate_id = String(required=True) profile_name = String(required=False) webhook_url = String(required=False) -class validate_json(Schema): +class ValidateJSON(Schema): crate_json = String(required=True) profile_name = String(required=False) @post_routes_bp.post("/validate_by_id") -@post_routes_bp.input(validate_data(partial=True), location='json') +@post_routes_bp.input(ValidateData(partial=True), location='json') def validate_ro_crate_from_id(json_data) -> tuple[Response, int]: """ Endpoint to validate an RO-Crate using its ID from MinIO. @@ -60,7 +60,7 @@ def validate_ro_crate_from_id(json_data) -> tuple[Response, int]: return queue_ro_crate_validation_task(crate_id, profile_name, webhook_url) @post_routes_bp.post("/validate_by_id_no_webhook") -@post_routes_bp.input(validate_data(partial=True), location='json') # -> json_data +@post_routes_bp.input(ValidateData(partial=True), location='json') # -> json_data def validate_ro_crate_from_id_no_webhook(json_data) -> tuple[Response, int]: """ Endpoint to validate an RO-Crate using its ID from MinIO. @@ -90,7 +90,7 @@ def validate_ro_crate_from_id_no_webhook(json_data) -> tuple[Response, int]: @post_routes_bp.post("/validate_metadata") -@post_routes_bp.input(validate_json(partial=True), location='json') # -> json_data +@post_routes_bp.input(ValidateJSON(partial=True), location='json') # -> json_data def validate_ro_crate_metadata(json_data) -> tuple[Response, int]: """ Endpoint to validate an RO-Crate JSON file uploaded to the Service. From aa6b6bdbaadf9a5963daf58854d170c0371109bb Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:21:06 +0100 Subject: [PATCH 06/14] tests without loading docker service --- .github/workflows/test_docker.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index d9f5ba6..bce7a70 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -8,11 +8,6 @@ jobs: test: runs-on: ubuntu-latest - services: - docker: - image: docker:24.0.6 - options: --privileged - steps: - name: Checkout code uses: actions/checkout@v4 From e1473cd71e2e5f7341617abefb9f62a3fee420fc Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:30:13 +0100 Subject: [PATCH 07/14] rename metadata_integration to test_integration --- .github/workflows/test_docker.yml | 2 +- tests/{metadata_integration.py => test_integration.py} | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename tests/{metadata_integration.py => test_integration.py} (96%) diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index bce7a70..9befbba 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -28,7 +28,7 @@ jobs: docker compose -f docker-compose-develop.yml build - name: Spin Up Docker Compose and Run Tests - run: pytest tests/metadata_integration.py -v + run: pytest tests/test_integration.py -v - name: Ensure that Docker Compose is Shutdown if: always() diff --git a/tests/metadata_integration.py b/tests/test_integration.py similarity index 96% rename from tests/metadata_integration.py rename to tests/test_integration.py index 2196ddd..d426403 100644 --- a/tests/metadata_integration.py +++ b/tests/test_integration.py @@ -22,7 +22,6 @@ def docker_compose(): subprocess.run(["docker", "compose", "down"], check=True) - def test_validate_metadata(): url = "http://localhost:5001/ro_crates/validate_metadata" headers = { @@ -50,4 +49,4 @@ def test_validate_metadata(): # Assertions — update based on expected API behavior assert response.status_code == 200 - assert response_result['passed'] == True \ No newline at end of file + assert response_result['passed'] is True From 113f4cbaa2eee0c59f6cc9615a1a281b9d08f213 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:52:22 +0100 Subject: [PATCH 08/14] tests for metadata api --- tests/test_metadata.py | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_metadata.py diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..4744606 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,63 @@ +from flask.testing import FlaskClient +import pytest +from unittest.mock import patch +from app import create_app + + +@pytest.fixture +def client(): + app = create_app() + return app.test_client() + + +def test_validate_metadata_with_all_fields(client: FlaskClient): + test_data = { + "crate_json": '{"@context": "https://w3id.org/ro/crate/1.1/context"}', + "profile_name": "default" + } + + with patch("app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task") as mock_queue: + mock_queue.return_value = ({"status": "success"}, 200) + + response = client.post("/ro_crates/validate_metadata", json=test_data) + print(response.json) + mock_queue.assert_called_once_with( + test_data["crate_json"], + test_data["profile_name"] + ) + assert response.status_code == 200 + assert response.json == {"status": "success"} + + +def test_validate_metadata_without_profile_name(client: FlaskClient): + """ + If the profile name is not specified then : + - 422 error + - Error message which includes 'Missing data for required field' + """ + test_data = { + "crate_json": '{"@context": "https://w3id.org/ro/crate/1.1/context"}' + } + + with patch("app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task") as mock_queue: + mock_queue.return_value = ({"status": "success"}, 200) + + response = client.post("/ro_crates/validate_metadata", json=test_data) + mock_queue.assert_called_once_with(test_data["crate_json"], None) + assert response.status_code == 200 + assert response.json == {"status": "success"} + + +def test_validate_metadata_missing_crate_json(client: FlaskClient): + """ + If the RO-Crate is missing APIFlask should return: + - 422 error + - Error message which includes 'Missing data for required field' + """ + test_data = { + "profile_name": "default" + } + + response = client.post("/ro_crates/validate_metadata", json=test_data) + assert response.status_code == 422 + assert "Missing data for required field" in response.get_data(as_text=True) From 1a82af53d092956185e1e1c63e0923ba3638d9e9 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:30:11 +0100 Subject: [PATCH 09/14] test docstrings --- tests/test_metadata.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 4744606..f39f36f 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -11,6 +11,12 @@ def client(): def test_validate_metadata_with_all_fields(client: FlaskClient): + """ + If both profile name and crate json are provided then processing + of the RO-Crate metadata should continue, and return: + - 200 status code + - JSON data object which includes "status" which is "success" + """ test_data = { "crate_json": '{"@context": "https://w3id.org/ro/crate/1.1/context"}', "profile_name": "default" @@ -31,9 +37,10 @@ def test_validate_metadata_with_all_fields(client: FlaskClient): def test_validate_metadata_without_profile_name(client: FlaskClient): """ - If the profile name is not specified then : - - 422 error - - Error message which includes 'Missing data for required field' + If the profile name is not specified then processing + of the RO-Crate metadata should continue, and return: + - 200 status code + - JSON data object which includes "status" which is "success" """ test_data = { "crate_json": '{"@context": "https://w3id.org/ro/crate/1.1/context"}' @@ -51,7 +58,7 @@ def test_validate_metadata_without_profile_name(client: FlaskClient): def test_validate_metadata_missing_crate_json(client: FlaskClient): """ If the RO-Crate is missing APIFlask should return: - - 422 error + - 422 status code - Error message which includes 'Missing data for required field' """ test_data = { From 89d0bca0d4e437769994e008556709e39d0458e5 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:47:52 +0100 Subject: [PATCH 10/14] switch API root from /ro_crates/ to /v1/ --- app/__init__.py | 6 +++--- app/ro_crates/routes/__init__.py | 10 ++-------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index cee8c91..184aaa1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,7 +9,7 @@ from apiflask import APIFlask from app.celery_worker import make_celery, celery -from app.ro_crates.routes import ro_crates_post_bp, ro_crates_get_bp +from app.ro_crates.routes import v1_post_bp, v1_get_bp from app.utils.config import DevelopmentConfig, ProductionConfig, make_celery @@ -20,8 +20,8 @@ def create_app() -> APIFlask: :return: Flask: A configured Flask application instance. """ app = APIFlask(__name__) - app.register_blueprint(ro_crates_post_bp, url_prefix="/ro_crates") - app.register_blueprint(ro_crates_get_bp, url_prefix="/ro_crates") + app.register_blueprint(v1_post_bp, url_prefix="/v1") + app.register_blueprint(v1_get_bp, url_prefix="/v1") # Load configuration: if os.getenv("FLASK_ENV") == "production": diff --git a/app/ro_crates/routes/__init__.py b/app/ro_crates/routes/__init__.py index 0a20b53..da8a903 100644 --- a/app/ro_crates/routes/__init__.py +++ b/app/ro_crates/routes/__init__.py @@ -4,14 +4,8 @@ # License: MIT # Copyright (c) 2025 eScience Lab, The University of Manchester -from apiflask import APIBlueprint - from app.ro_crates.routes.post_routes import post_routes_bp from app.ro_crates.routes.get_routes import get_routes_bp -#ro_crates_bp = APIBlueprint("ro_crates", __name__) - -#ro_crates_bp.register_blueprint(post_routes_bp, url_prefix="/post") - -ro_crates_post_bp = post_routes_bp -ro_crates_get_bp = get_routes_bp \ No newline at end of file +v1_post_bp = post_routes_bp +v1_get_bp = get_routes_bp From 8e42d9776f1344c63ba4294cb269316d693b4f20 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:48:18 +0100 Subject: [PATCH 11/14] switch API root to /v1/ in tests --- tests/test_integration.py | 2 +- tests/test_metadata.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index d426403..fda2b14 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -23,7 +23,7 @@ def docker_compose(): def test_validate_metadata(): - url = "http://localhost:5001/ro_crates/validate_metadata" + url = "http://localhost:5001/v1/validate_metadata" headers = { "accept": "application/json", "Content-Type": "application/json" diff --git a/tests/test_metadata.py b/tests/test_metadata.py index f39f36f..64401dc 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -25,7 +25,7 @@ def test_validate_metadata_with_all_fields(client: FlaskClient): with patch("app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task") as mock_queue: mock_queue.return_value = ({"status": "success"}, 200) - response = client.post("/ro_crates/validate_metadata", json=test_data) + response = client.post("/v1/validate_metadata", json=test_data) print(response.json) mock_queue.assert_called_once_with( test_data["crate_json"], @@ -49,7 +49,7 @@ def test_validate_metadata_without_profile_name(client: FlaskClient): with patch("app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task") as mock_queue: mock_queue.return_value = ({"status": "success"}, 200) - response = client.post("/ro_crates/validate_metadata", json=test_data) + response = client.post("/v1/validate_metadata", json=test_data) mock_queue.assert_called_once_with(test_data["crate_json"], None) assert response.status_code == 200 assert response.json == {"status": "success"} @@ -65,6 +65,6 @@ def test_validate_metadata_missing_crate_json(client: FlaskClient): "profile_name": "default" } - response = client.post("/ro_crates/validate_metadata", json=test_data) + response = client.post("/v1/validate_metadata", json=test_data) assert response.status_code == 422 assert "Missing data for required field" in response.get_data(as_text=True) From 15bf6851f93a1fe90b4c7f6c4583887648dfa5a5 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:16:45 +0100 Subject: [PATCH 12/14] update path in metadata tests --- tests/test_metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 64401dc..83efe04 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -25,7 +25,7 @@ def test_validate_metadata_with_all_fields(client: FlaskClient): with patch("app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task") as mock_queue: mock_queue.return_value = ({"status": "success"}, 200) - response = client.post("/v1/validate_metadata", json=test_data) + response = client.post("/v1/ro_crates/validate_metadata", json=test_data) print(response.json) mock_queue.assert_called_once_with( test_data["crate_json"], @@ -49,7 +49,7 @@ def test_validate_metadata_without_profile_name(client: FlaskClient): with patch("app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task") as mock_queue: mock_queue.return_value = ({"status": "success"}, 200) - response = client.post("/v1/validate_metadata", json=test_data) + response = client.post("/v1/ro_crates/validate_metadata", json=test_data) mock_queue.assert_called_once_with(test_data["crate_json"], None) assert response.status_code == 200 assert response.json == {"status": "success"} @@ -65,6 +65,6 @@ def test_validate_metadata_missing_crate_json(client: FlaskClient): "profile_name": "default" } - response = client.post("/v1/validate_metadata", json=test_data) + response = client.post("/v1/ro_crates/validate_metadata", json=test_data) assert response.status_code == 422 assert "Missing data for required field" in response.get_data(as_text=True) From 0e75e499057d19fb63602a18c9de712f9dd8a390 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:23:15 +0100 Subject: [PATCH 13/14] integration test for metadata updated to /vi/ro_crates path --- tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index fda2b14..bdf1030 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -23,7 +23,7 @@ def docker_compose(): def test_validate_metadata(): - url = "http://localhost:5001/v1/validate_metadata" + url = "http://localhost:5001/v1/ro_crates/validate_metadata" headers = { "accept": "application/json", "Content-Type": "application/json" From ad3295e8c6db7a4bf2ba8b0fcb63a1f0d53509c2 Mon Sep 17 00:00:00 2001 From: Douglas Lowe <10961945+douglowe@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:23:47 +0100 Subject: [PATCH 14/14] app path changed to /v1/ro_crates --- app/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 184aaa1..b67f34b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -20,8 +20,8 @@ def create_app() -> APIFlask: :return: Flask: A configured Flask application instance. """ app = APIFlask(__name__) - app.register_blueprint(v1_post_bp, url_prefix="/v1") - app.register_blueprint(v1_get_bp, url_prefix="/v1") + app.register_blueprint(v1_post_bp, url_prefix="/v1/ro_crates") + app.register_blueprint(v1_get_bp, url_prefix="/v1/ro_crates") # Load configuration: if os.getenv("FLASK_ENV") == "production":