Skip to content

Commit db4482c

Browse files
committed
refactor(validation): make metadata validation synchronous
- replaces metadata path with `run_metadata_validation()`, which validates inline via the runner and returns a `ValidationOutcome` (200 valid/invalid, 422 unvalidatable). - remove the metadata Celery task and `perform_metadata_validation`, which addresses the result.get() blocking and the return-in-finally bug. closes #169
1 parent ef93bdd commit db4482c

6 files changed

Lines changed: 96 additions & 361 deletions

File tree

app/ro_crates/routes/post_routes.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
"""Defines post API endpoints for validating RO-Crates using their IDs from MinIO."""
22

3-
# Author: Alexander Hambley
4-
# License: MIT
5-
# Copyright (c) 2025 eScience Lab, The University of Manchester
6-
73
from apiflask import APIBlueprint, Schema
84
from apiflask.fields import String, Boolean
95
from marshmallow.fields import Nested
106
from flask import Response, current_app
117

128
from app.services.validation_service import (
139
queue_ro_crate_validation_task,
14-
queue_ro_crate_metadata_validation_task,
10+
run_metadata_validation,
1511
)
1612

1713
# Always-on blueprint:
@@ -119,6 +115,6 @@ def validate_ro_crate_metadata(json_data) -> tuple[Response, int]:
119115

120116
profiles_path = current_app.config["PROFILES_PATH"]
121117

122-
return queue_ro_crate_metadata_validation_task(
118+
return run_metadata_validation(
123119
crate_json, profile_name, profiles_path=profiles_path
124120
)

app/services/validation_service.py

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
"""Service methods to queue RO-Crates for validation using the CRS4 validator and Celery."""
22

3-
# Author: Alexander Hambley
4-
# License: MIT
5-
# Copyright (c) 2025 eScience Lab, The University of Manchester
6-
73
import logging
84
import json
95

106
from flask import jsonify, Response
117

128
from app.tasks.validation_tasks import (
139
process_validation_task_by_id,
14-
process_validation_task_by_metadata,
1510
return_ro_crate_validation,
1611
check_ro_crate_exists,
17-
check_validation_exists
18-
)
12+
check_validation_exists,
13+
)
1914

2015
from app.utils.config import InvalidAPIUsage
2116
from app.utils.minio_utils import get_minio_client
22-
17+
from app.validation.runner import validate_metadata
18+
from app.validation.results import ValidationStatus
2319

2420
logger = logging.getLogger(__name__)
2521

2622

2723
def queue_ro_crate_validation_task(
28-
minio_config, crate_id, root_path=None, profile_name=None, webhook_url=None,
29-
profiles_path=None
24+
minio_config,
25+
crate_id,
26+
root_path=None,
27+
profile_name=None,
28+
webhook_url=None,
29+
profiles_path=None,
3030
) -> tuple[Response, int]:
3131
"""
3232
Queues an RO-Crate for validation with Celery.
@@ -52,55 +52,47 @@ def queue_ro_crate_validation_task(
5252
raise InvalidAPIUsage(f"No RO-Crate with prefix: {crate_id}", 400)
5353

5454
try:
55-
process_validation_task_by_id.delay(minio_config, crate_id, root_path,
56-
profile_name, webhook_url, profiles_path)
55+
process_validation_task_by_id.delay(
56+
minio_config, crate_id, root_path, profile_name, webhook_url, profiles_path
57+
)
5758
return jsonify({"message": "Validation in progress"}), 202
5859

5960
except Exception as e:
6061
return jsonify({"error": str(e)}), 500
6162

6263

63-
def queue_ro_crate_metadata_validation_task(
64-
crate_json: str, profile_name=None, webhook_url=None, profiles_path=None
64+
def run_metadata_validation(
65+
crate_json: str, profile_name=None, profiles_path=None
6566
) -> tuple[Response, int]:
6667
"""
67-
Queues an RO-Crate for validation with Celery.
68+
Validates RO-Crate metadata synchronously and returns the result inline.
6869
69-
:param crate_id: The ID of the RO-Crate to validate.
70+
Metadata-only validation is fast and stateless, so it runs in the request
71+
rather than via Celery. Returns 200 for a valid/invalid outcome and 422
72+
when the input cannot be validated (bad JSON, empty, or a validator error).
73+
74+
:param crate_json: The RO-Crate JSON-LD metadata, as a string.
7075
:param profile_name: The profile to validate against.
71-
:param webhook_url: The URL to POST the validation results to.
7276
:param profiles_path: A path to the profile definition directory.
7377
:return: A tuple containing a JSON response and an HTTP status code.
74-
:raises: Exception: If an error occurs whilst queueing the task.
7578
"""
7679

77-
logging.info(f"Processing: {crate_json}, {profile_name}, {webhook_url}")
78-
7980
if not crate_json:
8081
return jsonify({"error": "Missing required parameter: crate_json"}), 422
8182

8283
try:
83-
json_dict = json.loads(crate_json)
84-
except json.decoder.JSONDecodeError as err:
85-
return jsonify({"error": f"Required parameter crate_json is not valid JSON: {err}"}), 422
86-
else:
87-
if len(json_dict) == 0:
88-
return jsonify({"error": "Required parameter crate_json is empty"}), 422
84+
metadata = json.loads(crate_json)
85+
except json.JSONDecodeError as err:
86+
return jsonify({"error": f"crate_json is not valid JSON: {err}"}), 422
8987

90-
try:
91-
result = process_validation_task_by_metadata.delay(
92-
crate_json,
93-
profile_name,
94-
webhook_url,
95-
profiles_path
96-
)
97-
if webhook_url:
98-
return jsonify({"message": "Validation in progress"}), 202
99-
else:
100-
return jsonify({"result": result.get()}), 200
88+
if not metadata:
89+
return jsonify({"error": "Required parameter crate_json is empty"}), 422
10190

102-
except Exception as e:
103-
return jsonify({"error": str(e)}), 500
91+
outcome = validate_metadata(
92+
metadata, profile_name=profile_name, profiles_path=profiles_path
93+
)
94+
status_code = 422 if outcome.status is ValidationStatus.ERROR else 200
95+
return jsonify(outcome.to_dict()), status_code
10496

10597

10698
def get_ro_crate_validation_task(
@@ -127,10 +119,17 @@ def get_ro_crate_validation_task(
127119
logging.info("RO-Crate does not exist")
128120
raise InvalidAPIUsage(f"No RO-Crate with prefix: {crate_id}", 400)
129121

130-
if check_validation_exists(minio_client, minio_config["bucket"], crate_id, root_path):
122+
if check_validation_exists(
123+
minio_client, minio_config["bucket"], crate_id, root_path
124+
):
131125
logging.info("Validation result exists")
132126
else:
133127
logging.info("Validation does not exist")
134128
raise InvalidAPIUsage(f"No validation result yet for RO-Crate: {crate_id}", 400)
135129

136-
return return_ro_crate_validation(minio_client, minio_config["bucket"], crate_id, root_path), 200
130+
return (
131+
return_ro_crate_validation(
132+
minio_client, minio_config["bucket"], crate_id, root_path
133+
),
134+
200,
135+
)

app/tasks/validation_tasks.py

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
"""Tasks and helper methods for processing RO-Crate validation."""
22

3-
# Author: Alexander Hambley
4-
# License: MIT
5-
# Copyright (c) 2025 eScience Lab, The University of Manchester
6-
73
import logging
84
import os
95
import shutil
10-
import json
116
from typing import Optional
127

138
from rocrate_validator import services
@@ -110,61 +105,6 @@ def process_validation_task_by_id(
110105
shutil.rmtree(file_path)
111106

112107

113-
@celery.task
114-
def process_validation_task_by_metadata(
115-
crate_json: str,
116-
profile_name: str | None,
117-
webhook_url: str | None,
118-
profiles_path: Optional[str] = None,
119-
) -> ValidationResult | str:
120-
"""
121-
Background task to process the RO-Crate validation for a given json metadata string.
122-
123-
:param crate_json: A string containing the RO-Crate JSON metadata to validate.
124-
:param profile_name: The name of the validation profile to use. Defaults to None.
125-
:param webhook_url: The webhook URL to send notifications to. Defaults to None.
126-
:param profiles_path: The path to the profiles definition directory. Defaults to None.
127-
:raises Exception: If an error occurs during the validation process.
128-
129-
:todo: Replace the Crate ID with a more comprehensive system, and replace profile name with URI.
130-
"""
131-
132-
try:
133-
logging.info("Processing validation task for provided metadata string")
134-
135-
# Perform validation:
136-
validation_result = perform_metadata_validation(
137-
crate_json, profile_name, profiles_path=profiles_path
138-
)
139-
140-
if isinstance(validation_result, str):
141-
logging.error(f"Validation failed: {validation_result}")
142-
# TODO: Send webhook with failure notification
143-
raise Exception(f"Validation failed: {validation_result}")
144-
145-
if not validation_result.has_issues():
146-
logging.info("RO Crate metadata is valid.")
147-
else:
148-
logging.info("RO Crate metadata is invalid.")
149-
150-
if webhook_url:
151-
send_webhook_notification(webhook_url, validation_result.to_json())
152-
153-
except Exception as e:
154-
logging.error(f"Error processing validation task: {e}")
155-
156-
# Send failure notification via webhook
157-
error_data = {"profile_name": profile_name, "error": str(e)}
158-
if webhook_url:
159-
send_webhook_notification(webhook_url, error_data)
160-
161-
finally:
162-
if isinstance(validation_result, str):
163-
return validation_result
164-
else:
165-
return validation_result.to_json()
166-
167-
168108
def perform_ro_crate_validation(
169109
file_path: str,
170110
profile_name: str | None,
@@ -206,42 +146,6 @@ def perform_ro_crate_validation(
206146
return str(e)
207147

208148

209-
def perform_metadata_validation(
210-
crate_json: str,
211-
profile_name: str | None,
212-
skip_checks_list: Optional[list] = None,
213-
profiles_path: Optional[str] = None,
214-
) -> ValidationResult | str:
215-
"""
216-
Validates only RO-Crate metadata provided as a json string.
217-
218-
:param crate_json: The JSON string containing the metadata
219-
:param profile_name: The name of the validation profile to use. Defaults to None. If None, the CRS4 validator will
220-
attempt to determine the profile.
221-
:param profiles_path: The path to the profiles definition directory
222-
:param skip_checks_list: A list of checks to skip, if needed
223-
:return: The validation result.
224-
:raises Exception: If an error occurs during the validation process.
225-
"""
226-
227-
try:
228-
logging.info(f"Validating ro-crate metadata with profile {profile_name}")
229-
230-
settings = services.ValidationSettings(
231-
**({"metadata_only": True}),
232-
**({"metadata_dict": json.loads(crate_json)}),
233-
**({"profile_identifier": profile_name} if profile_name else {}),
234-
**({"skip_checks": skip_checks_list} if skip_checks_list else {}),
235-
**({"profiles_path": profiles_path} if profiles_path else {}),
236-
)
237-
238-
return services.validate(settings)
239-
240-
except Exception as e:
241-
logging.error(f"Unexpected error during validation: {e}")
242-
return str(e)
243-
244-
245149
def check_ro_crate_exists(
246150
minio_client: object,
247151
bucket_name: str,

tests/test_api_routes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def test_validate_metadata_success(
226226
profiles_path: str,
227227
):
228228
with patch(
229-
"app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task"
229+
"app.ro_crates.routes.post_routes.run_metadata_validation"
230230
) as mock_queue:
231231
mock_queue.return_value = (response_json, status_code)
232232

@@ -415,7 +415,7 @@ def test_minio_get_route_not_registered_when_disabled(client: FlaskClient):
415415
def test_metadata_route_available_when_minio_disabled(client: FlaskClient):
416416
payload = {"crate_json": '{"@context": "https://w3id.org/ro/crate/1.1/context"}'}
417417
with patch(
418-
"app.ro_crates.routes.post_routes.queue_ro_crate_metadata_validation_task"
418+
"app.ro_crates.routes.post_routes.run_metadata_validation"
419419
) as mock_queue:
420420
mock_queue.return_value = ({"status": "success"}, 200)
421421

0 commit comments

Comments
 (0)