Skip to content

Commit 7a0d28a

Browse files
committed
feat(validation): bump rocrate-validator to 0.10.0; rename entrypoint and image
A few final things to bundle together: - bump `roc-validator` 0.9.0 -> 0.10.0 to use offline/cache/extra-profiles - rename the WSGI entrypoint and align image/label naming. - Add `VALIDATION_OFFLINE`, `CACHE_PATH` and `EXTRA_PROFILES_PATH` settings - Offline validation is opt-in (default off). - `extra_profiles_path` ADDS to the bundled profiles (roc-validator 0.10.0 fix) - Warm validator HTTP cache at image build (rocrate-validator cache warm) - Replace the site-packages profile bake with extraction to /app/extra-profiles loaded via `EXTRA_PROFILES_PATH` - This is passed as a build arg from the profiles workflow. - Rename entrypoint `cratey.py` -> `wsgi.py` to fit convention - Rename the published image and the five-safes label to ro-crate-validation-service; point image.source at the renamed repo. Closes #162, #163, #164
1 parent 0b340ce commit 7a0d28a

18 files changed

Lines changed: 187 additions & 52 deletions

.github/workflows/build-with-profiles.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ jobs:
4040
with:
4141
context: .
4242
push: true
43-
# Bake the five-safes profile into the shared Dockerfile via build args.
43+
# Add the five-safes profile (via EXTRA_PROFILES_PATH) and warm its
44+
# cache, using the shared Dockerfile's build args.
4445
build-args: |
4546
FIVE_SAFES_PROFILE_VERSION=five-safes-0.7.4-beta
4647
PROFILES_ARCHIVE_URL=https://github.com/eScienceLab/rocrate-validator/archive/refs/tags/five-safes-0.7.4-beta.tar.gz
48+
EXTRA_PROFILES_PATH=/app/extra-profiles
4749
tags: ${{ steps.meta.outputs.tags }}
4850
labels: ${{ steps.meta.outputs.labels }}
4951

Dockerfile

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,38 @@ COPY requirements.txt .
99
RUN pip install --upgrade pip
1010
RUN pip install --no-cache-dir -r requirements.txt
1111

12-
COPY cratey.py LICENSE /app/
12+
COPY wsgi.py LICENSE /app/
1313
COPY app /app/app
1414

15-
# Optionally bake an extra RO-Crate profile into the validator's bundled
16-
# profiles directory. A plain build leaves PROFILES_ARCHIVE_URL empty and skips
17-
# this entirely; the "with profiles" image build passes these as --build-arg.
18-
# Baked profiles are then found automatically (no PROFILES_PATH needed).
15+
# Optionally fetch an extra RO-Crate profile into a normal directory. It is
16+
# *added* to the bundled profiles at runtime via EXTRA_PROFILES_PATH.
17+
# A plain build leaves PROFILES_ARCHIVE_URL empty
18+
# and skips this; the "with profiles" image build passes it as --build-arg.
1919
ARG PROFILES_ARCHIVE_URL=""
2020
ARG FIVE_SAFES_PROFILE_VERSION=""
21-
ARG PY_VER=3.11
21+
# Set EXTRA_PROFILES_PATH only for the profiles build (passed as a build arg).
22+
ARG EXTRA_PROFILES_PATH=""
23+
ENV EXTRA_PROFILES_PATH=${EXTRA_PROFILES_PATH}
24+
ENV CACHE_PATH=/app/.rocrate-cache
2225
RUN if [ -n "$PROFILES_ARCHIVE_URL" ]; then \
26+
mkdir -p /app/extra-profiles && \
2327
wget -O /tmp/profiles.tar.gz "$PROFILES_ARCHIVE_URL" && \
2428
tar -xzf /tmp/profiles.tar.gz \
25-
-C "/usr/local/lib/python${PY_VER}/site-packages/rocrate_validator/profiles/" \
29+
-C /app/extra-profiles \
2630
--strip-components=3 \
2731
"rocrate-validator-${FIVE_SAFES_PROFILE_VERSION}/rocrate_validator/profiles/five-safes-crate" && \
2832
rm /tmp/profiles.tar.gz ; \
2933
fi
3034

35+
# Pre-populate the HTTP cache so opt-in offline validation
36+
# (VALIDATION_OFFLINE=true) works without network at runtime.
37+
RUN if [ -n "$EXTRA_PROFILES_PATH" ]; then \
38+
rocrate-validator cache warm --all-profiles \
39+
--extra-profiles-path "$EXTRA_PROFILES_PATH" --cache-path "$CACHE_PATH" ; \
40+
else \
41+
rocrate-validator cache warm --all-profiles --cache-path "$CACHE_PATH" ; \
42+
fi
43+
3144
RUN useradd -ms /bin/bash flaskuser
3245
RUN chown -R flaskuser:flaskuser /app
3346

@@ -37,5 +50,5 @@ EXPOSE 5000
3750

3851
CMD ["flask", "run", "--host=0.0.0.0"]
3952

40-
LABEL org.opencontainers.image.source="https://github.com/eScienceLab/Cratey-Validator"
41-
LABEL org.cratey.five-safes-profile-version="${FIVE_SAFES_PROFILE_VERSION}"
53+
LABEL org.opencontainers.image.source="https://github.com/eScienceLab/RO-Crate-Validation-Service"
54+
LABEL org.ro-crate-validation-service.five-safes-profile-version="${FIVE_SAFES_PROFILE_VERSION}"

README.md

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,28 @@ fails quickly with a clear error rather than at the first request.
256256
| `S3_RESULTS_PREFIX` | `validation-results` | key prefix for results |
257257
| `CELERY_BROKER_URL` || Redis broker URL |
258258
| `CELERY_RESULT_BACKEND` || Celery result backend URL |
259-
| `PROFILES_PATH` || directory of 'custom' RO-Crate profiles (optional) |
259+
| `PROFILES_PATH` || directory of profiles that **replaces** the bundled set (optional) |
260+
| `EXTRA_PROFILES_PATH` || directory of profiles **added** to the bundled set (optional) |
261+
| `CACHE_PATH` | per-user dir | HTTP cache location for the validator |
262+
| `VALIDATION_OFFLINE` | `false` | validate using only the cache (no network) |
260263
| `FLASK_ENV` | `development` | `production` disables debug |
261264

262265
When `STORAGE_ENABLED=true`, the `S3_*` and `CELERY_*` variables above are
263266
**required** — startup fails if any are missing.
264267

268+
### Profiles, cache, and offline validation
269+
270+
Custom profiles (e.g. `five-safes-crate`) are best added with
271+
`EXTRA_PROFILES_PATH`, which **adds** them to the validator's bundled profiles —
272+
unlike `PROFILES_PATH`, which replaces the bundled set entirely. The published
273+
"with profiles" image bakes the five-safes profile in this way.
274+
275+
The validator caches the profile/context HTTP resources it fetches. The Docker
276+
image pre-populates this cache at build time (`rocrate-validator cache warm`),
277+
so setting `VALIDATION_OFFLINE=true` runs validation entirely from the cache
278+
with no network access. Online validation (the default) also uses and refreshes
279+
the cache. (Requires rocrate-validator ≥ 0.10.0.)
280+
265281
## Running the service
266282

267283
### Prerequisites
@@ -339,25 +355,25 @@ app/
339355
├── __init__.py # app factory: config, blueprints, error handlers, request IDs
340356
├── health.py # /healthz and /readyz
341357
├── storage/ # object-storage abstraction
342-
│ ├── base.py # StorageBackend protocol + ObjectStat
343-
│ ├── s3.py # boto3 implementation (any S3-compatible store)
344-
│ ├── memory.py # in-memory backend (tests / local)
345-
│ └── errors.py # StorageError, ObjectNotFound
358+
│ ├── base.py # StorageBackend protocol + ObjectStat
359+
│ ├── s3.py # boto3 implementation (any S3-compatible store)
360+
│ ├── memory.py # in-memory backend (tests / local)
361+
│ └── errors.py # StorageError, ObjectNotFound
346362
├── crates/ # crate identity, layout, resolution
347-
│ ├── ids.py # 'Crate ID' validation
348-
│ ├── layout.py # object keys
349-
│ └── resolver.py # deterministic zip/dir resolution
363+
│ ├── ids.py # 'Crate ID' validation
364+
│ ├── layout.py # object keys
365+
│ └── resolver.py # deterministic zip/dir resolution
350366
├── validation/ # validation boundary
351-
│ ├── results.py # ValidationOutcome (valid/invalid/error)
352-
│ └── runner.py # wraps rocrate-validator
353-
├── ro_crates/routes/ # HTTP endpoints (metadata + ID-based)
367+
│ ├── results.py # ValidationOutcome (valid/invalid/error)
368+
│ └── runner.py # wraps rocrate-validator
369+
├── ro_crates/routes/ # HTTP endpoints (metadata + ID-based)
354370
├── services/
355-
│ ├── validation_service.py # request handling: resolve, queue, read results
356-
│ └── logging_service.py # JSON logging, request IDs, redaction
357-
├── tasks/validation_tasks.py # Celery task: fetch → validate → persist → webhook
371+
│ ├── validation_service.py # request handling: resolve, queue, read results
372+
│ └── logging_service.py # JSON logging, request IDs, redaction
373+
├── tasks/validation_tasks.py # Celery task: fetch → validate → persist → webhook
358374
└── utils/
359-
├── config.py # validated Settings
360-
└── webhook_utils.py # webhook delivery with retry/backoff
375+
├── config.py # validated Settings
376+
└── webhook_utils.py # webhook delivery with retry/backoff
361377
```
362378

363379
## License

app/ro_crates/routes/post_routes.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,15 @@ def validate_ro_crate_metadata(json_data) -> tuple[Response, int]:
7171
"""
7272

7373
crate_json = json_data["crate_json"]
74+
profile_name = json_data.get("profile_name")
7475

75-
if "profile_name" in json_data:
76-
profile_name = json_data["profile_name"]
77-
else:
78-
profile_name = None
79-
80-
profiles_path = current_app.config["PROFILES_PATH"]
76+
settings = current_app.config["SETTINGS"]
8177

82-
return run_metadata_validation(crate_json, profile_name, profiles_path=profiles_path)
78+
return run_metadata_validation(
79+
crate_json,
80+
profile_name,
81+
profiles_path=settings.profiles_path,
82+
extra_profiles_path=settings.extra_profiles_path,
83+
cache_path=settings.cache_path,
84+
offline=settings.validation_offline,
85+
)

app/services/validation_service.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ def queue_ro_crate_validation_task(
5151

5252

5353
def run_metadata_validation(
54-
crate_json: str, profile_name=None, profiles_path=None
54+
crate_json: str,
55+
profile_name=None,
56+
profiles_path=None,
57+
extra_profiles_path=None,
58+
cache_path=None,
59+
offline=False,
5560
) -> tuple[Response, int]:
5661
"""
5762
Validate RO-Crate metadata synchronously and return the result inline.
@@ -62,7 +67,10 @@ def run_metadata_validation(
6267
6368
:param crate_json: The RO-Crate JSON-LD metadata, as a string.
6469
:param profile_name: The profile to validate against.
65-
:param profiles_path: A path to the profile definition directory.
70+
:param profiles_path: A profiles directory that replaces the bundled set.
71+
:param extra_profiles_path: A profiles directory added to the bundled set.
72+
:param cache_path: HTTP cache location for the validator.
73+
:param offline: Validate using only the cache (no network).
6674
:return: A JSON response and HTTP status code.
6775
"""
6876
if not crate_json:
@@ -76,7 +84,14 @@ def run_metadata_validation(
7684
if not metadata:
7785
return jsonify({"error": "Required parameter crate_json is empty"}), 422
7886

79-
outcome = validate_metadata(metadata, profile_name=profile_name, profiles_path=profiles_path)
87+
outcome = validate_metadata(
88+
metadata,
89+
profile_name=profile_name,
90+
profiles_path=profiles_path,
91+
extra_profiles_path=extra_profiles_path,
92+
cache_path=cache_path,
93+
offline=offline,
94+
)
8095
status_code = 422 if outcome.status is ValidationStatus.ERROR else 200
8196
return jsonify(outcome.to_dict()), status_code
8297

app/tasks/validation_tasks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ def run_validation_job(
8585
local_path,
8686
profile_name=profile_name,
8787
profiles_path=settings.profiles_path,
88+
extra_profiles_path=settings.extra_profiles_path,
89+
cache_path=settings.cache_path,
90+
offline=settings.validation_offline,
8891
created_at=created_at,
8992
)
9093
finally:

app/utils/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class Settings:
4747
s3_use_ssl: bool
4848
s3_crate_prefix: str
4949
s3_results_prefix: str
50+
extra_profiles_path: Optional[str]
51+
cache_path: Optional[str]
52+
validation_offline: bool
5053

5154
@classmethod
5255
def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "Settings":
@@ -91,6 +94,9 @@ def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "Settings":
9194
s3_use_ssl=_parse_bool(env.get("S3_USE_SSL")),
9295
s3_crate_prefix=_clean(env.get("S3_CRATE_PREFIX")) or "crates",
9396
s3_results_prefix=_clean(env.get("S3_RESULTS_PREFIX")) or "validation-results",
97+
extra_profiles_path=_clean(env.get("EXTRA_PROFILES_PATH")),
98+
cache_path=_clean(env.get("CACHE_PATH")),
99+
validation_offline=_parse_bool(env.get("VALIDATION_OFFLINE")),
94100
)
95101

96102

app/validation/runner.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,21 @@ def validate_crate_path(
1919
rocrate_uri: str,
2020
profile_name: Optional[str] = None,
2121
profiles_path: Optional[str] = None,
22+
extra_profiles_path: Optional[str] = None,
2223
skip_checks: Optional[list] = None,
24+
cache_path: Optional[str] = None,
25+
offline: bool = False,
2326
created_at: Optional[str] = None,
2427
) -> ValidationOutcome:
2528
"""Validate a crate on disk (a directory or zip) at ``rocrate_uri``."""
2629
return _run(
2730
{"rocrate_uri": rocrate_uri},
2831
profile_name=profile_name,
2932
profiles_path=profiles_path,
33+
extra_profiles_path=extra_profiles_path,
3034
skip_checks=skip_checks,
35+
cache_path=cache_path,
36+
offline=offline,
3137
created_at=created_at,
3238
)
3339

@@ -36,15 +42,21 @@ def validate_metadata(
3642
metadata: dict,
3743
profile_name: Optional[str] = None,
3844
profiles_path: Optional[str] = None,
45+
extra_profiles_path: Optional[str] = None,
3946
skip_checks: Optional[list] = None,
47+
cache_path: Optional[str] = None,
48+
offline: bool = False,
4049
created_at: Optional[str] = None,
4150
) -> ValidationOutcome:
4251
"""Validate an in-memory RO-Crate metadata graph."""
4352
return _run(
4453
{"metadata_only": True, "metadata_dict": metadata},
4554
profile_name=profile_name,
4655
profiles_path=profiles_path,
56+
extra_profiles_path=extra_profiles_path,
4757
skip_checks=skip_checks,
58+
cache_path=cache_path,
59+
offline=offline,
4860
created_at=created_at,
4961
)
5062

@@ -53,16 +65,25 @@ def _run(
5365
base_settings: dict,
5466
profile_name: Optional[str],
5567
profiles_path: Optional[str],
68+
extra_profiles_path: Optional[str],
5669
skip_checks: Optional[list],
70+
cache_path: Optional[str],
71+
offline: bool,
5772
created_at: Optional[str],
5873
) -> ValidationOutcome:
5974
options = dict(base_settings)
6075
if profile_name:
6176
options["profile_identifier"] = profile_name
6277
if profiles_path:
6378
options["profiles_path"] = profiles_path
79+
if extra_profiles_path:
80+
options["extra_profiles_path"] = extra_profiles_path
6481
if skip_checks:
6582
options["skip_checks"] = skip_checks
83+
if cache_path:
84+
options["cache_path"] = cache_path
85+
if offline:
86+
options["offline"] = offline
6687

6788
try:
6889
settings = services.ValidationSettings(**options)

docker-compose-develop.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ services:
1111
ports:
1212
- "5001:5000"
1313
environment:
14-
- FLASK_APP=cratey.py
14+
- FLASK_APP=wsgi.py
1515
- FLASK_ENV=development
1616
- CELERY_BROKER_URL=redis://redis:6379/0
1717
- CELERY_RESULT_BACKEND=redis://redis:6379/0
@@ -23,7 +23,7 @@ services:
2323
- S3_SECRET_KEY=${S3_SECRET_KEY}
2424
- S3_BUCKET=${S3_BUCKET}
2525
- S3_USE_SSL=${S3_USE_SSL:-false}
26-
- PROFILES_PATH=/app/profiles
26+
- EXTRA_PROFILES_PATH=/app/profiles
2727
depends_on:
2828
- redis
2929
# Metadata validation runs synchronously in this process, so the flask
@@ -47,7 +47,7 @@ services:
4747
- S3_SECRET_KEY=${S3_SECRET_KEY}
4848
- S3_BUCKET=${S3_BUCKET}
4949
- S3_USE_SSL=${S3_USE_SSL:-false}
50-
- PROFILES_PATH=/app/profiles
50+
- EXTRA_PROFILES_PATH=/app/profiles
5151
depends_on:
5252
- redis
5353
volumes:

docker-compose.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Default stack, using the published image (ghcr.io/.../cratey-validator).
1+
# Default stack, using the published image (ghcr.io/.../ro-crate-validation-service).
22
# For running the service. Object storage is opt-in: set STORAGE_ENABLED=true
33
# and start the "objectstore" profile (docker compose --profile objectstore up).
44
# For local development against a freshly built image, use
@@ -7,11 +7,11 @@
77
services:
88
flask:
99
platform: linux/x86_64
10-
image: "ghcr.io/esciencelab/cratey-validator:0.1"
10+
image: "ghcr.io/esciencelab/ro-crate-validation-service:0.1"
1111
ports:
1212
- "5001:5000"
1313
environment:
14-
- FLASK_APP=cratey.py
14+
- FLASK_APP=wsgi.py
1515
- FLASK_ENV=development
1616
- CELERY_BROKER_URL=redis://redis:6379/0
1717
- CELERY_RESULT_BACKEND=redis://redis:6379/0
@@ -28,7 +28,7 @@ services:
2828

2929
celery_worker:
3030
platform: linux/x86_64
31-
image: "ghcr.io/esciencelab/cratey-validator:0.1"
31+
image: "ghcr.io/esciencelab/ro-crate-validation-service:0.1"
3232
command: celery -A app.celery_worker.celery worker --loglevel=info -E
3333
environment:
3434
- CELERY_BROKER_URL=redis://redis:6379/0

0 commit comments

Comments
 (0)