Skip to content

eScienceLab/RO-Crate-Validation-Service

Repository files navigation

RO-Crate Validation Service

A Flask + Celery service that validates RO-Crates against RO-Crate profiles using the rocrate-validator library.

An RO-Crate is a way of packaging research data together with structured, machine-readable metadata (a JSON-LD file named ro-crate-metadata.json). Validating a crate checks that metadata against a profile (a set of requirements, e.g. the base ro-crate profile or a domain profile such as five-safes-crate) and reports whether it conforms.

The service works in two modes:

  • Metadata-only (default): validate an RO-Crate metadata document supplied directly in the request. This is stateless, so nothing is stored.
  • Storage-backed (optional): validate whole crates (zip or directory) held in an S3-compatible object store, asynchronously, and store the results.

Architecture

flowchart LR
    Client([Client])
    API["Flask API (apiflask)"]
    Worker["Celery worker"]
    Broker[("Redis broker")]
    Validator["rocrate-validator"]
    Store[("S3-compatible store<br/>e.g., RustFS / AWS S3")]

    Client -->|HTTP| API
    API -->|"Crate metadata-only flow: validate inline"| Validator
    API -->|"Crate ID flow: resolve, then queue"| Broker
    Broker --> Worker
    Worker --> Validator
    API -->|"resolve / read result"| Store
    Worker -->|"fetch crate / write result"| Store
    Worker -.->|"optional webhook"| Client
Loading
  • Flask API handles HTTP. Metadata-only validation runs inline (stateless). S3-backed requests are validated by the Celery worker.
  • Redis is the Celery broker.
  • S3-compatible store holds crates and validation results. Credentials live server-side (the service is configured with them); clients never send storage credentials. Any S3-compatible store should work β€” the dev stack uses RustFS.

Concepts

Crate ID

In the S3 flow, a crate is addressed by a Crate ID. This a short, opaque label chosen by the caller (e.g. my-dataset-2026). It is not a filename, a path, or a URL: the service composes the actual object keys from it.

Crate IDs are validated strictly: they must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ (start alphanumeric; only letters, digits, ., _, -; max 128 characters; no / or ..). This:

  • keeps IDs safe to compose into object keys and local paths;
  • means the ID is treated as a single opaque token β€” a .zip inside an ID is harmless, because the service never parses meaning out of the ID.

Invalid IDs are rejected with 400 before anything else happens.

Storage layout

Crates and their results live under separate, configurable prefixes in one bucket, so a result can never be confused with (or collide with) a crate:

Item Object key
Crate (zip) {S3_CRATE_PREFIX}/{id}.zip
Crate (directory) {S3_CRATE_PREFIX}/{id}/ containing ro-crate-metadata.json
Validation result {S3_RESULTS_PREFIX}/{id}.json

Defaults: S3_CRATE_PREFIX=crates, S3_RESULTS_PREFIX=validation-results.

Zip layout matters: for a zip crate, ro-crate-metadata.json must be at the root of the archive. Zipping a folder (so entries look like mycrate/ro-crate-metadata.json) makes the crate invalid. Zip the crate's contents, not its containing folder.

Crate resolution

The service resolves a Crate ID to a concrete object by direct existence checks on the canonical keys, rather than by listing a prefix and assuming. This makes resolution deterministic and unambiguous:

Where the zip object is {prefix}/{id}.zip and the directory metadata is {prefix}/{id}/ro-crate-metadata.json:

flowchart TD
    A["crate_id"] --> B{"valid format?"}
    B -- no --> E["400 Invalid crate ID"]
    B -- yes --> C{"zip object exists?"}
    C -- yes --> Z{"directory metadata also exists?"}
    Z -- yes --> AMB["409 Ambiguous"]
    Z -- no --> ZIP["resolve as zip crate"]
    C -- no --> D{"directory metadata exists?"}
    D -- yes --> DIR["resolve as directory crate"]
    D -- no --> NF["404 Not found"]
Loading

Validation outcome

Every validation produces a single outcome object with an explicit status:

status meaning
valid the crate/metadata conforms to the profile
invalid it was validated but has conformance issues (see detail)
error it could not be validated (bad input, validator failure)
{ "status": "invalid", "profile": "ro-crate-1.2", "created_at": "…", "detail": { … } }

Request flows

Metadata-only (synchronous)

sequenceDiagram
    actor Client
    participant API as Flask API
    participant V as rocrate-validator
    Client->>API: POST /v1/ro_crates/validate_metadata { crate_json }
    API->>V: validate metadata (inline)
    V-->>API: outcome
    API-->>Client: 200 valid/invalid Β· 422 error/bad input
Loading

S3-backed (asynchronous)

sequenceDiagram
    actor Client
    participant API as Flask API
    participant S as S3 store
    participant Q as Redis
    participant W as Celery worker
    Client->>API: POST /v1/ro_crates/{id}/validation
    API->>S: resolve crate (stat canonical keys)
    alt invalid id / not found / ambiguous
        API-->>Client: 400 / 404 / 409
    else exists
        API->>Q: queue (id, profile?, webhook?)
        API-->>Client: 202 Validation in progress
        Q->>W: deliver task
        W->>S: download crate
        W->>W: validate
        W->>S: persist {results_prefix}/{id}.json
        opt webhook_url given
            W-->>Client: POST outcome (retried with backoff)
        end
    end
    Client->>API: GET /v1/ro_crates/{id}/validation
    API->>S: read result object
    API-->>Client: 200 outcome Β· 404 not validated yet
Loading

The worker runs the stages in order (fetch β†’ validate β†’ persist β†’ webhook), so a storage write failure can never trigger a "success" webhook, and the outcome (including an error outcome) is always persisted so GET reflects it.

API

Base URL in the dev stack: http://localhost:5001.

POST /v1/ro_crates/validate_metadata

Validate an RO-Crate metadata document inline. This is always available.

field required type description
crate_json yes string RO-Crate metadata JSON-LD, as a string
profile_name no string profile to validate against (default: auto/base profile)

Responses: 200 (valid/invalid outcome), 422 (missing/empty/invalid JSON, or an error outcome).

crate_json is the metadata document as a string, so the easiest way to validate a file is to let jq read and escape it (-R raw, -s slurp), and then post the result:

jq -Rs '{crate_json: .}' ro-crate-metadata.json \
  | curl -X POST http://localhost:5001/v1/ro_crates/validate_metadata \
      -H 'Content-Type: application/json' -d @-

Add a profile with jq -Rs '{crate_json: ., profile_name: "ro-crate-1.2"}' ….

Or inline a small document directly:

curl -X POST http://localhost:5001/v1/ro_crates/validate_metadata \
  -H 'Content-Type: application/json' \
  -d '{"crate_json": "{\"@context\": \"https://w3id.org/ro/crate/1.2/context\", \"@graph\": []}"}'

POST /v1/ro_crates/{crate_id}/validation

Queue a stored crate for validation. This is only registered when storage is enabled (otherwise 404). The request body carries no credentials.

field required type description
profile_name no string profile to validate against
webhook_url no string URL to POST the result to when done

Responses: 202 queued, 400 invalid ID, 404 crate not found, 409 ambiguous (both zip and directory exist) in the object store, 503 storage unavailable.

curl -X POST http://localhost:5001/v1/ro_crates/my-crate/validation \
  -H 'Content-Type: application/json' -d '{"profile_name": "ro-crate-1.2"}'

GET /v1/ro_crates/{crate_id}/validation

Fetch a stored validation result. Only registered when storage is enabled.

Responses: 200 (the stored outcome, including a persisted error outcome), 400 invalid ID, 404 no result stored yet.

curl http://localhost:5001/v1/ro_crates/my-crate/validation

Health

  • GET /healthz - liveness; 200 {"status": "ok"} whenever the process is up.
  • GET /readyz - readiness; checks the object store and Celery broker. 200 when ready/available, 503 otherwise. When storage is disabled, those dependencies report disabled.

Configuration

Configuration is read once at the start and validated; a misconfigured deployment fails quickly with a clear error rather than at the first request.

config default description
STORAGE_ENABLED false enable the S3 ID endpoints
S3_ENDPOINT β€” object store endpoint, e.g. objectstore:9000
S3_ACCESS_KEY β€” access key
S3_SECRET_KEY β€” secret key
S3_BUCKET β€” bucket holding crates and results
S3_USE_SSL false use HTTPS to the store
S3_REGION us-east-1 region (for AWS; not used elsewhere)
S3_CRATE_PREFIX crates key prefix for crates
S3_RESULTS_PREFIX validation-results key prefix for results
CELERY_BROKER_URL β€” Redis broker URL
CELERY_RESULT_BACKEND β€” Celery result backend URL
PROFILES_PATH β€” directory of profiles that replaces the bundled set (optional)
EXTRA_PROFILES_PATH β€” directory of profiles added to the bundled set (optional)
CACHE_PATH per-user dir HTTP cache location for the validator
VALIDATION_OFFLINE false validate using only the cache (no network)
FLASK_ENV development production disables debug

When STORAGE_ENABLED=true, the S3_* and CELERY_* variables above are required β€” startup fails if any are missing.

Profiles, cache, and offline validation

Custom profiles (e.g. five-safes-crate) are best added with EXTRA_PROFILES_PATH, which adds them to the validator's bundled profiles β€” unlike PROFILES_PATH, which replaces the bundled set entirely. The published "with profiles" image bakes the five-safes profile in this way.

The validator caches the profile/context HTTP resources it fetches. The Docker image pre-populates this cache at build time (rocrate-validator cache warm), so setting VALIDATION_OFFLINE=true runs validation entirely from the cache with no network access. Online validation (the default) also uses and refreshes the cache. (Requires rocrate-validator β‰₯ 0.10.0.)

Running the service

Prerequisites

  • Docker with Docker Compose

Quick start

git clone https://github.com/eScienceLab/RO-Crate-Validation-Service.git
cd RO-Crate-Validation-Service
cp example.env .env          # and then edit credentials
docker compose up --build

This runs in metadata-only mode (no storage). The API is at http://localhost:5001.

With object storage (RustFS)

Set STORAGE_ENABLED=true in .env, then start the local object store with its opt-in profile (uses the prebuilt image compose file, or -f docker-compose-develop.yml to build locally):

docker compose --profile objectstore up --build

The RustFS console is at http://localhost:9001 (default credentials are rustfsadmin / rustfsadmin). Create the bucket named in S3_BUCKET (default ro-crates) and upload crates under the crate prefix (crates/<id>.zip or crates/<id>/…).

The service does not create the bucket; create it once via the console, the RustFS Web UI, AWS CLI, or boto3.

Custom profiles

To validate against profiles other than the bundled ones, mount a profiles directory into both the flask and worker containers (metadata validation runs in flask; crate validation runs in the worker) and set PROFILES_PATH to the mounted path. See docker-compose-develop.yml for a working example.

Development

docker compose -f docker-compose-develop.yml --profile objectstore up --build

Dependencies

Direct dependencies live in pyproject.toml.

pip-compile pyproject.toml -o requirements.txt                   # runtime lock
pip-compile --extra dev pyproject.toml -o requirements-dev.txt   # + dev tools

Tests & linting

pip install -r requirements-dev.txt
pytest --ignore=tests/test_integration.py     # unit tests (no Docker needed)
pytest tests/test_integration.py              # integration tests (needs Docker)
ruff check . && ruff format --check .         # lint + format

tests/ mirrors the app/ package layout. The integration tests bring up the compose stack and seed crates via boto3.

Project structure

app/
β”œβ”€β”€ __init__.py                 # app factory: config, blueprints, error handlers, request IDs
β”œβ”€β”€ health.py                   # /healthz and /readyz
β”œβ”€β”€ storage/                    # object-storage abstraction
β”‚   β”œβ”€β”€ base.py                 #   StorageBackend protocol + ObjectStat
β”‚   β”œβ”€β”€ s3.py                   #   boto3 implementation (any S3-compatible store)
β”‚   β”œβ”€β”€ memory.py               #   in-memory backend (tests / local)
β”‚   └── errors.py               #   StorageError, ObjectNotFound
β”œβ”€β”€ crates/                     # crate identity, layout, resolution
β”‚   β”œβ”€β”€ ids.py                  #   'Crate ID' validation
β”‚   β”œβ”€β”€ layout.py               #   object keys
β”‚   └── resolver.py             #   deterministic zip/dir resolution
β”œβ”€β”€ validation/                 # validation boundary
β”‚   β”œβ”€β”€ results.py              #   ValidationOutcome (valid/invalid/error)
β”‚   └── runner.py               #   wraps rocrate-validator
β”œβ”€β”€ ro_crates/routes/           #   HTTP endpoints (metadata + ID-based)
β”œβ”€β”€ services/
β”‚   β”œβ”€β”€ validation_service.py   #   request handling: resolve, queue, read results
β”‚   └── logging_service.py      #   JSON logging, request IDs, redaction
β”œβ”€β”€ tasks/validation_tasks.py   #   Celery task: fetch β†’ validate β†’ persist β†’ webhook
└── utils/
    β”œβ”€β”€ config.py               #   validated Settings
    └── webhook_utils.py        #   webhook delivery with retry/backoff

License

MIT β€” Β© eScience Lab, The University of Manchester.

About

An API for validating RO Crates in federated trusted research environments πŸ•΅οΈπŸ“¦

Resources

License

Stars

1 star

Watchers

7 watching

Forks

Packages

 
 
 

Contributors