Skip to content

Commit fa1ef14

Browse files
committed
refactor(storage): migrate store-backed flow to S3Backend; drop minio-py
- Move POST/task/GET onto StorageBackend + resolver + runner with server-side credentials. POST resolves before queueing (400/404/409/503 via app error handlers); - the Celery task (`run_validation_job`) runs fetch->validate->persist->webhook with retries and persists error outcomes; GET reads the results prefix. - Add s3_crate_prefix/s3_results_prefix to Settings. Delete minio_utils and test_minio, remove minio from requirements and recompile the lock. - Covers the store-backed validation flow Closes #177, #178, #179
1 parent db4482c commit fa1ef14

14 files changed

Lines changed: 447 additions & 2074 deletions

app/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
from apiflask import APIFlask
66

7+
from app.crates.ids import InvalidCrateId
8+
from app.crates.resolver import CrateNotFound, AmbiguousCrate
79
from app.ro_crates.routes import v1_post_bp, v1_minio_post_bp, v1_minio_get_bp
10+
from app.storage.errors import StorageError
811
from app.utils.config import (
912
Settings,
1013
InvalidAPIUsage,
@@ -58,6 +61,23 @@ def create_app(settings: Settings | None = None) -> APIFlask:
5861
def invalid_api_usage(e):
5962
return jsonify(e.to_dict()), e.status_code
6063

64+
@app.errorhandler(InvalidCrateId)
65+
def invalid_crate_id(e):
66+
return jsonify({"error": str(e)}), 400
67+
68+
@app.errorhandler(CrateNotFound)
69+
def crate_not_found(e):
70+
return jsonify({"error": str(e)}), 404
71+
72+
@app.errorhandler(AmbiguousCrate)
73+
def ambiguous_crate(e):
74+
return jsonify({"error": str(e)}), 409
75+
76+
@app.errorhandler(StorageError)
77+
def storage_error(e):
78+
logger.error("Storage error: %s", e)
79+
return jsonify({"error": "Storage backend unavailable"}), 503
80+
6181
# Integrate Celery
6282
make_celery(app)
6383

app/ro_crates/routes/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
# Always registered:
1111
v1_post_bp = post_routes_bp
1212

13-
# Registered only when MinIO is enabled:
13+
# Registered only when object storage is enabled:
1414
v1_minio_post_bp = minio_post_routes_bp
1515
v1_minio_get_bp = get_routes_bp

app/ro_crates/routes/get_routes.py

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,24 @@
1-
"""Defines get API endpoints for validating RO-Crates using their IDs from MinIO."""
1+
"""GET endpoint for retrieving a stored RO-Crate validation result by ID."""
22

3-
# Author: Alexander Hambley
4-
# License: MIT
5-
# Copyright (c) 2025 eScience Lab, The University of Manchester
6-
7-
from apiflask import APIBlueprint, Schema
8-
from apiflask.fields import String, Boolean
9-
from marshmallow.fields import Nested
3+
from apiflask import APIBlueprint
104
from flask import Response
115

126
from app.services.validation_service import get_ro_crate_validation_task
137

148
get_routes_bp = APIBlueprint("get_routes", __name__)
159

1610

17-
class MinioConfig(Schema):
18-
endpoint = String(required=True)
19-
accesskey = String(required=True)
20-
secret = String(required=True)
21-
ssl = Boolean(required=True)
22-
bucket = String(required=True)
23-
24-
25-
class ValidateResult(Schema):
26-
minio_config = Nested(MinioConfig, required=True)
27-
root_path = String(required=False)
28-
29-
3011
@get_routes_bp.get("<string:crate_id>/validation")
31-
@get_routes_bp.input(ValidateResult(partial=False), location='json')
32-
def get_ro_crate_validation_by_id(json_data, crate_id) -> tuple[Response, int]:
12+
def get_ro_crate_validation_by_id(crate_id) -> tuple[Response, int]:
3313
"""
34-
Endpoint to obtain an RO-Crate validation result using its ID from MinIO.
14+
Obtain a stored RO-Crate validation result by its ID.
3515
3616
Path Parameters:
3717
- **crate_id**: The RO-Crate ID. _Required_.
3818
39-
Request Body Parameters:
40-
- **minio_config**: The MinIO bucket containing the RO-Crate. _Required_
41-
- **endpoint**: Endpoint, e.g. 'localhost:9000'
42-
- **accesskey**: Access key / username
43-
- **secret**: Secret / password
44-
- **ssl**: Use SSL encryption? True/False
45-
- **bucket**: The MinIO bucket to access
46-
- **root_path**: The root path containing the RO-Crate. _Optional_
47-
4819
Returns:
49-
- A tuple containing the validation result and an HTTP status code.
50-
51-
Raises:
52-
- KeyError: If required parameters (`crate_id`) are missing.
20+
- A tuple containing the stored validation result and an HTTP status code.
21+
Returns 404 if no result has been stored for the crate yet.
5322
"""
5423

55-
minio_config = json_data["minio_config"]
56-
57-
if "root_path" in json_data:
58-
root_path = json_data["root_path"]
59-
else:
60-
root_path = None
61-
62-
return get_ro_crate_validation_task(minio_config, crate_id, root_path)
24+
return get_ro_crate_validation_task(crate_id)

app/ro_crates/routes/post_routes.py

Lines changed: 11 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
"""Defines post API endpoints for validating RO-Crates using their IDs from MinIO."""
1+
"""POST endpoints for validating RO-Crates by stored ID or by inline metadata."""
22

33
from apiflask import APIBlueprint, Schema
4-
from apiflask.fields import String, Boolean
5-
from marshmallow.fields import Nested
4+
from apiflask.fields import String
65
from flask import Response, current_app
76

87
from app.services.validation_service import (
@@ -13,22 +12,12 @@
1312
# Always-on blueprint:
1413
post_routes_bp = APIBlueprint("post_routes", __name__)
1514

16-
# MinIO blueprint. Only registered when MINIO_ENABLED is true
15+
# Store-backed blueprint. Only registered when storage is enabled
1716
# (see app.create_app), so the ID-based routes are unreachable by default.
1817
minio_post_routes_bp = APIBlueprint("minio_post_routes", __name__)
1918

2019

21-
class MinioConfig(Schema):
22-
endpoint = String(required=True)
23-
accesskey = String(required=True)
24-
secret = String(required=True)
25-
ssl = Boolean(required=True)
26-
bucket = String(required=True)
27-
28-
2920
class ValidateCrate(Schema):
30-
minio_config = Nested(MinioConfig, required=True)
31-
root_path = String(required=False)
3221
profile_name = String(required=False)
3322
webhook_url = String(required=False)
3423

@@ -42,51 +31,26 @@ class ValidateJSON(Schema):
4231
@minio_post_routes_bp.input(ValidateCrate(partial=False), location="json")
4332
def validate_ro_crate_via_id(json_data, crate_id) -> tuple[Response, int]:
4433
"""
45-
Endpoint to validate an RO-Crate using its ID from MinIO.
34+
Validate a stored RO-Crate by its ID.
35+
36+
Storage credentials and layout are configured server-side; the request body
37+
carries only optional fields.
4638
4739
Path Parameters:
4840
- **crate_id**: The RO-Crate ID. _Required_.
4941
5042
Request Body Parameters:
51-
- **minio_config**: The MinIO bucket containing the RO-Crate. _Required_
52-
- **endpoint**: Endpoint, e.g. 'localhost:9000'
53-
- **accesskey**: Access key / username
54-
- **secret**: Secret / password
55-
- **ssl**: Use SSL encryption? True/False
56-
- **bucket**: The MinIO bucket to access
57-
- **root_path**: The root path containing the RO-Crate. _Optional_
5843
- **profile_name**: The profile name for validation. _Optional_.
59-
- **webhook_url**: The webhook URL where validation results will be sent. _Optional_.
44+
- **webhook_url**: The webhook URL where the validation result will be sent. _Optional_.
6045
6146
Returns:
6247
- A tuple containing the validation task's response and an HTTP status code.
63-
64-
Raises:
65-
- KeyError: If required parameters (`crate_id` or `webhook_url`) are missing.
6648
"""
6749

68-
minio_config = json_data["minio_config"]
69-
70-
if "root_path" in json_data:
71-
root_path = json_data["root_path"]
72-
else:
73-
root_path = None
74-
75-
if "webhook_url" in json_data:
76-
webhook_url = json_data["webhook_url"]
77-
else:
78-
webhook_url = None
50+
profile_name = json_data.get("profile_name")
51+
webhook_url = json_data.get("webhook_url")
7952

80-
if "profile_name" in json_data:
81-
profile_name = json_data["profile_name"]
82-
else:
83-
profile_name = None
84-
85-
profiles_path = current_app.config["PROFILES_PATH"]
86-
87-
return queue_ro_crate_validation_task(
88-
minio_config, crate_id, root_path, profile_name, webhook_url, profiles_path
89-
)
53+
return queue_ro_crate_validation_task(crate_id, profile_name, webhook_url)
9054

9155

9256
@post_routes_bp.post("/validate_metadata")

app/services/validation_service.py

Lines changed: 52 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,60 @@
1-
"""Service methods to queue RO-Crates for validation using the CRS4 validator and Celery."""
1+
"""Service layer for RO-Crate validation requests."""
22

3-
import logging
43
import json
4+
import logging
55

6-
from flask import jsonify, Response
7-
8-
from app.tasks.validation_tasks import (
9-
process_validation_task_by_id,
10-
return_ro_crate_validation,
11-
check_ro_crate_exists,
12-
check_validation_exists,
13-
)
6+
from flask import jsonify, Response, current_app
147

8+
from app.crates.ids import validate_crate_id
9+
from app.crates.layout import result_key
10+
from app.crates.resolver import resolve_crate
11+
from app.storage.errors import ObjectNotFound
12+
from app.storage.s3 import S3Backend
13+
from app.tasks.validation_tasks import process_validation_task_by_id
1514
from app.utils.config import InvalidAPIUsage
16-
from app.utils.minio_utils import get_minio_client
17-
from app.validation.runner import validate_metadata
1815
from app.validation.results import ValidationStatus
16+
from app.validation.runner import validate_metadata
1917

2018
logger = logging.getLogger(__name__)
2119

2220

21+
def _build_storage() -> S3Backend:
22+
"""Build the storage backend from server-side settings."""
23+
settings = current_app.config["SETTINGS"]
24+
return S3Backend.from_settings(settings)
25+
26+
2327
def queue_ro_crate_validation_task(
24-
minio_config,
25-
crate_id,
26-
root_path=None,
27-
profile_name=None,
28-
webhook_url=None,
29-
profiles_path=None,
28+
crate_id: str, profile_name=None, webhook_url=None
3029
) -> tuple[Response, int]:
3130
"""
32-
Queues an RO-Crate for validation with Celery.
31+
Resolve a crate by ID and queue it for asynchronous validation.
32+
33+
Credentials and layout are server-side; the request carries only the ID and
34+
optional profile/webhook. Resolution happens before queueing so a bad or
35+
missing crate is reported immediately. ``InvalidCrateId`` / ``CrateNotFound``
36+
/ ``AmbiguousCrate`` / ``StorageError`` propagate to the app error handlers.
3337
34-
:param minio_config: Access settings for Minio instance containing the RO-Crate.
3538
:param crate_id: The ID of the RO-Crate to validate.
36-
:param root_path: The root path containing the RO-Crate.
3739
:param profile_name: The profile to validate against.
38-
:param webhook_url: The URL to POST the validation results to.
39-
:return: A tuple containing a JSON response and an HTTP status code.
40-
:raises: Exception: If an error occurs whilst queueing the task.
40+
:param webhook_url: The URL to POST the validation result to.
41+
:return: A JSON response and HTTP status code.
4142
"""
43+
settings = current_app.config["SETTINGS"]
44+
storage = _build_storage()
4245

43-
logging.info(f"Processing: {crate_id}, {profile_name}, {webhook_url}")
44-
logging.info(f"Minio Bucket: {minio_config['bucket']}; Root path: {root_path}")
45-
46-
minio_client = get_minio_client(minio_config)
46+
# Raises if the crate is missing/ambiguous/invalid -> handled as 4xx.
47+
resolve_crate(storage, crate_id, settings.s3_crate_prefix)
4748

48-
if check_ro_crate_exists(minio_client, minio_config["bucket"], crate_id, root_path):
49-
logging.info("RO-Crate exists")
50-
else:
51-
logging.info("RO-Crate does not exist")
52-
raise InvalidAPIUsage(f"No RO-Crate with prefix: {crate_id}", 400)
53-
54-
try:
55-
process_validation_task_by_id.delay(
56-
minio_config, crate_id, root_path, profile_name, webhook_url, profiles_path
57-
)
58-
return jsonify({"message": "Validation in progress"}), 202
59-
60-
except Exception as e:
61-
return jsonify({"error": str(e)}), 500
49+
process_validation_task_by_id.delay(crate_id, profile_name, webhook_url)
50+
return jsonify({"message": "Validation in progress"}), 202
6251

6352

6453
def run_metadata_validation(
6554
crate_json: str, profile_name=None, profiles_path=None
6655
) -> tuple[Response, int]:
6756
"""
68-
Validates RO-Crate metadata synchronously and returns the result inline.
57+
Validate RO-Crate metadata synchronously and return the result inline.
6958
7059
Metadata-only validation is fast and stateless, so it runs in the request
7160
rather than via Celery. Returns 200 for a valid/invalid outcome and 422
@@ -74,9 +63,8 @@ def run_metadata_validation(
7463
:param crate_json: The RO-Crate JSON-LD metadata, as a string.
7564
:param profile_name: The profile to validate against.
7665
:param profiles_path: A path to the profile definition directory.
77-
:return: A tuple containing a JSON response and an HTTP status code.
66+
:return: A JSON response and HTTP status code.
7867
"""
79-
8068
if not crate_json:
8169
return jsonify({"error": "Missing required parameter: crate_json"}), 422
8270

@@ -95,41 +83,25 @@ def run_metadata_validation(
9583
return jsonify(outcome.to_dict()), status_code
9684

9785

98-
def get_ro_crate_validation_task(
99-
minio_config: dict,
100-
crate_id: str,
101-
root_path: str,
102-
) -> tuple[Response, int]:
86+
def get_ro_crate_validation_task(crate_id: str) -> tuple[Response, int]:
10387
"""
104-
Retrieves an RO-Crate validation result.
88+
Return a crate's stored validation result.
10589
106-
:param minio_config: Access settings for Minio instance containing the RO-Crate.
107-
:param crate_id: The ID of the RO-Crate to validate.
108-
:param root_path: The root path containing the RO-Crate.
109-
:return: A tuple containing a JSON response and an HTTP status code.
110-
:raises Exception: If an error occurs whilst retreiving validation result
90+
Reads the result object from the results prefix. A missing result yields a
91+
404; the stored outcome (including a persisted ``error`` outcome) is returned
92+
as-is otherwise.
93+
94+
:param crate_id: The ID of the RO-Crate whose result is requested.
95+
:return: A JSON response and HTTP status code.
11196
"""
112-
logging.info(f"Retrieving validation for: {crate_id}")
113-
114-
minio_client = get_minio_client(minio_config)
115-
116-
if check_ro_crate_exists(minio_client, minio_config["bucket"], crate_id, root_path):
117-
logging.info("RO-Crate exists")
118-
else:
119-
logging.info("RO-Crate does not exist")
120-
raise InvalidAPIUsage(f"No RO-Crate with prefix: {crate_id}", 400)
121-
122-
if check_validation_exists(
123-
minio_client, minio_config["bucket"], crate_id, root_path
124-
):
125-
logging.info("Validation result exists")
126-
else:
127-
logging.info("Validation does not exist")
128-
raise InvalidAPIUsage(f"No validation result yet for RO-Crate: {crate_id}", 400)
129-
130-
return (
131-
return_ro_crate_validation(
132-
minio_client, minio_config["bucket"], crate_id, root_path
133-
),
134-
200,
135-
)
97+
settings = current_app.config["SETTINGS"]
98+
storage = _build_storage()
99+
100+
validate_crate_id(crate_id) # raises InvalidCrateId -> 400
101+
102+
try:
103+
data = storage.get_bytes(result_key(settings.s3_results_prefix, crate_id))
104+
except ObjectNotFound:
105+
raise InvalidAPIUsage(f"No validation result yet for RO-Crate: {crate_id}", 404)
106+
107+
return jsonify(json.loads(data)), 200

0 commit comments

Comments
 (0)