Skip to content

Commit 4fb8605

Browse files
committed
validate_metadata api
1 parent 99bcb1b commit 4fb8605

4 files changed

Lines changed: 192 additions & 3 deletions

File tree

app/ro_crates/routes/post_routes.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
from apiflask.fields import Integer, String
99
from flask import request, Response
1010

11-
from app.services.validation_service import queue_ro_crate_validation_task
11+
from app.services.validation_service import (
12+
queue_ro_crate_validation_task,
13+
queue_ro_crate_metadata_validation_task
14+
)
1215

1316
post_routes_bp = APIBlueprint("post_routes", __name__)
1417

@@ -17,6 +20,10 @@ class validate_data(Schema):
1720
profile_name = String(required=False)
1821
webhook_url = String(required=False)
1922

23+
class validate_json(Schema):
24+
crate_json = String(required=True)
25+
profile_name = String(required=False)
26+
2027

2128
@post_routes_bp.post("/validate_by_id")
2229
@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]:
8087
profile_name = None
8188

8289
return queue_ro_crate_validation_task(crate_id, profile_name)
90+
91+
92+
@post_routes_bp.post("/validate_metadata")
93+
@post_routes_bp.input(validate_json(partial=True), location='json') # -> json_data
94+
def validate_ro_crate_metadata(json_data) -> tuple[Response, int]:
95+
"""
96+
Endpoint to validate an RO-Crate JSON file uploaded to the Service.
97+
98+
Parameters:
99+
- **crate_json**: The RO-Crate JSON-LD, as a string. _Required_
100+
- **profile_name**: The profile name for validation. _Optional_.
101+
102+
Returns:
103+
- A tuple containing the validation task's response and an HTTP status code.
104+
105+
Raises:
106+
- KeyError: If required parameters (`crate_json`) are missing.
107+
"""
108+
109+
try:
110+
crate_json = json_data['crate_json']
111+
except:
112+
raise KeyError("Missing required parameter: 'crate_json'")
113+
114+
try:
115+
profile_name = json_data['profile_name']
116+
except:
117+
profile_name = None
118+
119+
return queue_ro_crate_metadata_validation_task(crate_json, profile_name)
120+

app/services/validation_service.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from app.tasks.validation_tasks import (
1212
process_validation_task_by_id,
13+
process_validation_task_by_metadata,
1314
return_ro_crate_validation
1415
)
1516

@@ -42,6 +43,39 @@ def queue_ro_crate_validation_task(
4243
return jsonify({"error": str(e)}), 500
4344

4445

46+
def queue_ro_crate_metadata_validation_task(
47+
crate_json, profile_name=None, webhook_url=None
48+
) -> tuple[Response, int]:
49+
"""
50+
Queues an RO-Crate for validation with Celery.
51+
52+
:param crate_id: The ID of the RO-Crate to validate.
53+
:param profile_name: The profile to validate against.
54+
:param webhook_url: The URL to POST the validation results to.
55+
:return: A tuple containing a JSON response and an HTTP status code.
56+
:raises: Exception: If an error occurs whilst queueing the task.
57+
"""
58+
59+
logging.info(f"Processing: {crate_json}, {profile_name}, {webhook_url}")
60+
61+
if not crate_json:
62+
return jsonify({"error": "Missing required parameter: crate_json"}), 400
63+
64+
try:
65+
result = process_validation_task_by_metadata.delay(
66+
crate_json,
67+
profile_name,
68+
webhook_url
69+
)
70+
if webhook_url:
71+
return jsonify({"message": "Validation in progress"}), 202
72+
else:
73+
return jsonify({"result": result.get()}), 200
74+
75+
except Exception as e:
76+
return jsonify({"error": str(e)}), 500
77+
78+
4579
def get_ro_crate_validation_task(
4680
crate_id
4781
) -> tuple[Response, int]:

app/tasks/validation_tasks.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import logging
88
import os
9+
import shutil
10+
from typing import Optional
911

1012
from rocrate_validator import services
1113
from rocrate_validator.models import ValidationResult
@@ -17,6 +19,7 @@
1719
get_validation_status_from_minio
1820
)
1921
from app.utils.webhook_utils import send_webhook_notification
22+
from app.utils.file_utils import build_metadata_only_rocrate
2023

2124
logger = logging.getLogger(__name__)
2225

@@ -78,15 +81,75 @@ def process_validation_task_by_id(
7881
os.remove(file_path)
7982

8083

84+
@celery.task
85+
def process_validation_task_by_metadata(
86+
crate_json: str, profile_name: str | None, webhook_url: str | None
87+
) -> ValidationResult | str:
88+
"""
89+
Background task to process the RO-Crate validation for a given json metadata string.
90+
91+
:param crate_json: A string containing the RO-Crate JSON metadata to validate.
92+
:param profile_name: The name of the validation profile to use. Defaults to None.
93+
:param webhook_url: The webhook URL to send notifications to. Defaults to None.
94+
:raises Exception: If an error occurs during the validation process.
95+
96+
:todo: Replace the Crate ID with a more comprehensive system, and replace profile name with URI.
97+
"""
98+
99+
skip_checks_list = ['ro-crate-1.1_12.1']
100+
file_path = None
101+
102+
try:
103+
# Fetch the RO-Crate from MinIO using the provided ID:
104+
file_path = build_metadata_only_rocrate(crate_json)
105+
106+
logging.info(f"Processing validation task for {file_path}")
107+
108+
# Perform validation:
109+
validation_result = perform_ro_crate_validation(file_path,
110+
profile_name,
111+
skip_checks_list
112+
)
113+
114+
if isinstance(validation_result, str):
115+
logging.error(f"Validation failed: {validation_result}")
116+
# TODO: Send webhook with failure notification
117+
raise Exception(f"Validation failed: {validation_result}")
118+
119+
if not validation_result.has_issues():
120+
logging.info(f"RO Crate {file_path} is valid.")
121+
else:
122+
logging.info(f"RO Crate {file_path} is invalid.")
123+
124+
if webhook_url:
125+
send_webhook_notification(webhook_url, validation_result.to_json())
126+
127+
except Exception as e:
128+
logging.error(f"Error processing validation task: {e}")
129+
130+
# Send failure notification via webhook
131+
error_data = {"profile_name": profile_name, "error": str(e)}
132+
send_webhook_notification(webhook_url, error_data)
133+
134+
finally:
135+
# Clean up the temporary file if it was created:
136+
if file_path and os.path.exists(file_path):
137+
shutil.rmtree(file_path)
138+
139+
return validation_result.to_json()
140+
141+
142+
81143
def perform_ro_crate_validation(
82-
file_path: str, profile_name: str | None
144+
file_path: str, profile_name: str | None, skip_checks_list: Optional[list] = None
83145
) -> ValidationResult | str:
84146
"""
85147
Validates an RO-Crate using the provided file path and profile name.
86148
87149
:param file_path: The path to the RO-Crate file to validate
88150
:param profile_name: The name of the validation profile to use. Defaults to None. If None, the CRS4 validator will
89151
attempt to determine the profile.
152+
:param skip_checks_list: A list of checks to skip, if needed
90153
:return: The validation result.
91154
:raises Exception: If an error occurs during the validation process.
92155
"""
@@ -102,8 +165,8 @@ def perform_ro_crate_validation(
102165
)
103166
settings = services.ValidationSettings(
104167
rocrate_uri=full_file_path,
105-
# Only include profile_identifier if the profile_name is provided:
106168
**({"profile_identifier": profile_name} if profile_name else {}),
169+
**({"skip_checks": skip_checks_list} if skip_checks_list else {})
107170
)
108171

109172
return services.validate(settings)

app/utils/file_utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Utility methods for interacting with the File System."""
2+
3+
# Author: Douglas Lowe, Alexander Hambley
4+
# License: MIT
5+
# Copyright (c) 2025 eScience Lab, The University of Manchester
6+
7+
import json
8+
import logging
9+
import os
10+
import tempfile
11+
12+
from dotenv import load_dotenv
13+
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
def build_metadata_only_rocrate(crate_json: str) -> str:
19+
"""
20+
Creates a temporary directory for an empty RO-Crate,
21+
and saves the JSON string as a metadata file.
22+
23+
:param crate_json: The metadata string.
24+
:return: The local file path where the RO-Crate is saved.
25+
:raises ValueError: If the required environment variables are not set.
26+
:raises Exception: If an unexpected error occurs during the operation.
27+
"""
28+
29+
load_dotenv()
30+
31+
try:
32+
# Prepare temporary file path to store RO Crate for validation:
33+
temp_dir = tempfile.mkdtemp()
34+
file_path = os.path.join(temp_dir, 'ro-crate-metadata.json')
35+
36+
logging.info(
37+
f"Creating RO-Crate Metadata file. File path: {file_path}"
38+
)
39+
with open(file_path, 'w') as f:
40+
f.write(crate_json)
41+
logging.info(
42+
f"RO-Crate metadata successfully saved to {file_path}."
43+
)
44+
45+
return temp_dir
46+
47+
except ValueError as value_error:
48+
logging.error(f"Configuration Error: {value_error}")
49+
raise
50+
51+
except Exception as e:
52+
logging.error(f"Unexpected error creating RO-Crate metadata: {e}")
53+
raise
54+

0 commit comments

Comments
 (0)