diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index ab2226ce..a23bb72f 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -10,6 +10,8 @@ jobs: strategy: matrix: ngff: + - '0.1' + - '0.2' - '0.3' runs-on: ubuntu-20.04 steps: diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..53de2396 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*/.DS_Store +**/__pycache__/* \ No newline at end of file diff --git a/0.1/examples/image/invalid/invalid_channels_color.json b/0.1/examples/image/invalid/invalid_channels_color.json new file mode 100644 index 00000000..da301713 --- /dev/null +++ b/0.1/examples/image/invalid/invalid_channels_color.json @@ -0,0 +1,31 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.1", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ] + } + ], + "omero": { + "channels": [ + { + "active": true, + "coefficient": 1.0, + "color": 255, + "family": "linear", + "label": "1234", + "window": { + "end": 1765.0, + "max": 2555.0, + "min": 5.0, + "start": 0.0 + } + } + ] + } +} diff --git a/0.1/examples/image/invalid/invalid_channels_window.json b/0.1/examples/image/invalid/invalid_channels_window.json new file mode 100644 index 00000000..534b6eb6 --- /dev/null +++ b/0.1/examples/image/invalid/invalid_channels_window.json @@ -0,0 +1,31 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ] + } + ], + "omero": { + "channels": [ + { + "active": true, + "coefficient": 1.0, + "color": "ff0000", + "family": "linear", + "label": "1234", + "window": { + "end": "100", + "max": 2555.0, + "min": 5.0, + "start": 0.0 + } + } + ] + } +} diff --git a/0.1/examples/image/invalid/invalid_path.json b/0.1/examples/image/invalid/invalid_path.json new file mode 100644 index 00000000..33d8b9ac --- /dev/null +++ b/0.1/examples/image/invalid/invalid_path.json @@ -0,0 +1,17 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.1", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + }, + { + "path": 0 + } + ] + } + ] +} diff --git a/0.1/examples/image/invalid/missing_datasets.json b/0.1/examples/image/invalid/missing_datasets.json new file mode 100644 index 00000000..ea5eee52 --- /dev/null +++ b/0.1/examples/image/invalid/missing_datasets.json @@ -0,0 +1,9 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.1", + "name": "example" + } + ] +} diff --git a/0.1/examples/image/invalid/missing_path.json b/0.1/examples/image/invalid/missing_path.json new file mode 100644 index 00000000..df9c74cc --- /dev/null +++ b/0.1/examples/image/invalid/missing_path.json @@ -0,0 +1,17 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.1", + "name": "example", + "datasets": [ + { + "foo": "path/to/0" + }, + { + "path": "1" + } + ] + } + ] +} diff --git a/0.1/examples/image/invalid/no_datasets.json b/0.1/examples/image/invalid/no_datasets.json new file mode 100644 index 00000000..e519c9c7 --- /dev/null +++ b/0.1/examples/image/invalid/no_datasets.json @@ -0,0 +1,15 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.1", + "name": "example", + "datasets": [], + "axes": [ + "z", + "y", + "x" + ] + } + ] +} diff --git a/0.1/examples/image/invalid/no_multiscales.json b/0.1/examples/image/invalid/no_multiscales.json new file mode 100644 index 00000000..d6dbfb33 --- /dev/null +++ b/0.1/examples/image/invalid/no_multiscales.json @@ -0,0 +1,4 @@ +{ + "@type": "ngff:Image", + "multiscales": [] +} diff --git a/0.1/examples/image/valid/image.json b/0.1/examples/image/valid/image.json new file mode 100644 index 00000000..5c749323 --- /dev/null +++ b/0.1/examples/image/valid/image.json @@ -0,0 +1,12 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "datasets": [ + {"path": "path/to/0"}, + {"path": "1"}, + {"path": "2"} + ] + } + ] +} diff --git a/0.1/examples/image/valid/image_complete.json b/0.1/examples/image/valid/image_complete.json new file mode 100644 index 00000000..080448a2 --- /dev/null +++ b/0.1/examples/image/valid/image_complete.json @@ -0,0 +1,58 @@ +{ + "@id": "#my-image", + "@type": "ngff:Image", + "multiscales": [ + { + "@id": "#my-pyramid", + "version": "0.1", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + }, + { + "path": "1" + }, + { + "path": "2" + } + ], + "type": "gaussian", + "metadata": { + "method": "skimage.transform.pyramid_gaussian", + "version": "0.16.1", + "args": [ + "true", + "false" + ], + "kwargs": { + "multichannel": true + } + } + } + ], + "omero": { + "id": 1, + "version": "0.1", + "channels": [ + { + "active": true, + "color": "0000FF", + "family": "linear", + "inverted": false, + "label": "1234", + "window": { + "end": 1765.0, + "max": 2555.0, + "min": 5.0, + "start": 0.0 + } + } + ], + "rdefs": { + "defaultZ": 0, + "defaultT": 0, + "model": "color" + } + } +} diff --git a/0.1/examples/image/valid/image_complete_no_omero.json b/0.1/examples/image/valid/image_complete_no_omero.json new file mode 100644 index 00000000..a2669b48 --- /dev/null +++ b/0.1/examples/image/valid/image_complete_no_omero.json @@ -0,0 +1,28 @@ +{ + "@id": "top", + "@type": "ngff:Image", + "multiscales": [ + { + "@id": "inner", + "version": "0.1", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ], + "type": "gaussian", + "metadata": { + "method": "skimage.transform.pyramid_gaussian", + "version": "0.16.1", + "args": [ + "true", + "false" + ], + "kwargs": { + "multichannel": true + } + } + } + ] +} diff --git a/0.1/examples/image/valid/missing_name.json b/0.1/examples/image/valid/missing_name.json new file mode 100644 index 00000000..fe3de034 --- /dev/null +++ b/0.1/examples/image/valid/missing_name.json @@ -0,0 +1,18 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.1", + "datasets": [ + { + "path": "path/to/0" + } + ], + "type": "gaussian", + "metadata": { + "method": "skimage.transform.pyramid_gaussian", + "version": "0.16.1" + } + } + ] +} diff --git a/0.1/examples/image/valid/missing_type.json b/0.1/examples/image/valid/missing_type.json new file mode 100644 index 00000000..b9ece803 --- /dev/null +++ b/0.1/examples/image/valid/missing_type.json @@ -0,0 +1,20 @@ +{ + "@id": "#my-image", + "@type": "ngff:Image", + "multiscales": [ + { + "@id": "#my-pyramid", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ], + "metadata": { + "method": "skimage.transform.pyramid_gaussian", + "version": "0.16.1" + }, + "version": "0.1" + } + ] +} \ No newline at end of file diff --git a/0.1/examples/image/valid/missing_version.json b/0.1/examples/image/valid/missing_version.json new file mode 100644 index 00000000..51086c4b --- /dev/null +++ b/0.1/examples/image/valid/missing_version.json @@ -0,0 +1,39 @@ +{ + "@id": "#my-image", + "@type": "ngff:Image", + "multiscales": [ + { + "@id": "#my-pyramid", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ], + "type": "gaussian", + "metadata": { + "method": "skimage.transform.pyramid_gaussian", + "version": "0.16.1" + } + } + ], + "omero": { + "id": 1, + "version": "0.1", + "channels": [ + { + "active": true, + "color": "0000FF", + "family": "linear", + "inverted": false, + "label": "1234", + "window": { + "end": 1765.0, + "max": 2555.0, + "min": 5.0, + "start": 0.0 + } + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/empty_wells.json b/0.1/examples/plate/invalid/empty_wells.json new file mode 100644 index 00000000..1c8861da --- /dev/null +++ b/0.1/examples/plate/invalid/empty_wells.json @@ -0,0 +1,16 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "rows": [ + { + "name": "A" + } + ], + "version": "0.1", + "wells": [] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/invalid_fieldcount.json b/0.1/examples/plate/invalid/invalid_fieldcount.json new file mode 100644 index 00000000..89540652 --- /dev/null +++ b/0.1/examples/plate/invalid/invalid_fieldcount.json @@ -0,0 +1,31 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "field_count": 1, + "name": "plate name", + "rows": [ + { + "name": "A" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + } + ], + "acquisitions": [ + { + "id": 1, + "maximumfieldcount": "2" + }, + { + "id": 2 + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/missing_acquisition_id.json b/0.1/examples/plate/invalid/missing_acquisition_id.json new file mode 100644 index 00000000..46fe420a --- /dev/null +++ b/0.1/examples/plate/invalid/missing_acquisition_id.json @@ -0,0 +1,35 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "field_count": 1, + "name": "plate name", + "rows": [ + { + "name": "A" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + } + ], + "acquisitions": [ + { + "maximumfieldcount": 2, + "name": "Meas_01(2012-07-31_10-41-12)", + "starttime": 1343731272000 + }, + { + "id": 2, + "maximumfieldcount": 2, + "name": "Meas_02(201207-31_11-56-41)", + "starttime": 1343735801000 + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/missing_column_name.json b/0.1/examples/plate/invalid/missing_column_name.json new file mode 100644 index 00000000..dbf4e2d6 --- /dev/null +++ b/0.1/examples/plate/invalid/missing_column_name.json @@ -0,0 +1,18 @@ +{ + "plate": { + "columns": [ + {} + ], + "rows": [ + { + "name": "A" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/missing_columns.json b/0.1/examples/plate/invalid/missing_columns.json new file mode 100644 index 00000000..997e2927 --- /dev/null +++ b/0.1/examples/plate/invalid/missing_columns.json @@ -0,0 +1,15 @@ +{ + "plate": { + "rows": [ + { + "name": "A" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/missing_rows.json b/0.1/examples/plate/invalid/missing_rows.json new file mode 100644 index 00000000..74f6ba6a --- /dev/null +++ b/0.1/examples/plate/invalid/missing_rows.json @@ -0,0 +1,15 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/missing_version.json b/0.1/examples/plate/invalid/missing_version.json new file mode 100644 index 00000000..36b0afde --- /dev/null +++ b/0.1/examples/plate/invalid/missing_version.json @@ -0,0 +1,19 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "rows": [ + { + "name": "A" + } + ], + "wells": [ + { + "path": "A/1" + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/missing_well_path.json b/0.1/examples/plate/invalid/missing_well_path.json new file mode 100644 index 00000000..ded5f769 --- /dev/null +++ b/0.1/examples/plate/invalid/missing_well_path.json @@ -0,0 +1,18 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "rows": [ + { + "name": "A" + } + ], + "version": "0.1", + "wells": [ + {} + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/invalid/missing_wells.json b/0.1/examples/plate/invalid/missing_wells.json new file mode 100644 index 00000000..fd00f136 --- /dev/null +++ b/0.1/examples/plate/invalid/missing_wells.json @@ -0,0 +1,15 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "rows": [ + { + "name": "A" + } + ], + "version": "0.1" + } +} \ No newline at end of file diff --git a/0.1/examples/plate/valid/minimal_acquisition.json b/0.1/examples/plate/valid/minimal_acquisition.json new file mode 100644 index 00000000..43037600 --- /dev/null +++ b/0.1/examples/plate/valid/minimal_acquisition.json @@ -0,0 +1,31 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + } + ], + "field_count": 1, + "name": "plate name", + "rows": [ + { + "name": "A" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + } + ], + "acquisitions": [ + { + "id": 1, + "name": "SHOULD have a name" + }, + { + "id": 2 + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/valid/plate.json b/0.1/examples/plate/valid/plate.json new file mode 100644 index 00000000..eb2bd031 --- /dev/null +++ b/0.1/examples/plate/valid/plate.json @@ -0,0 +1,40 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + }, + { + "name": "2" + }, + { + "name": "3" + } + ], + "field_count": 1, + "name": "plate name", + "rows": [ + { + "name": "A" + }, + { + "name": "B" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + }, + { + "path": "B/2" + }, + { + "path": "A/1" + }, + { + "path": "B/3" + } + ] + } +} \ No newline at end of file diff --git a/0.1/examples/plate/valid/plate_acquisition.json b/0.1/examples/plate/valid/plate_acquisition.json new file mode 100644 index 00000000..0559909b --- /dev/null +++ b/0.1/examples/plate/valid/plate_acquisition.json @@ -0,0 +1,55 @@ +{ + "plate": { + "columns": [ + { + "name": "1" + }, + { + "name": "2" + }, + { + "name": "3" + } + ], + "field_count": 1, + "name": "plate name", + "rows": [ + { + "name": "A" + }, + { + "name": "B" + } + ], + "version": "0.1", + "wells": [ + { + "path": "A/3" + }, + { + "path": "B/2" + }, + { + "path": "A/1" + }, + { + "path": "B/3" + } + ], + "acquisitions": [ + { + "id": 1, + "description": "First Acquisition", + "maximumfieldcount": 2, + "name": "Meas_01(2012-07-31_10-41-12)", + "starttime": 1343731272000 + }, + { + "id": 2, + "maximumfieldcount": 2, + "name": "Meas_02(201207-31_11-56-41)", + "starttime": 1343735801000 + } + ] + } +} \ No newline at end of file diff --git a/0.3/schemas/json_schema/image.schema b/0.1/schemas/image.schema similarity index 76% rename from 0.3/schemas/json_schema/image.schema rename to 0.1/schemas/image.schema index 6b2562ce..5a97f3f2 100644 --- a/0.3/schemas/json_schema/image.schema +++ b/0.1/schemas/image.schema @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:8000/image.schema", + "$id": "https://ngff.openmicroscopy.org/0.1/schemas/image.schema", "title": "NGFF Image", "description": "JSON from OME-NGFF .zattrs", "type": "object", @@ -26,45 +26,27 @@ }, "required": ["path"] } - } - }, - "oneOf": [ - { - "type": "object", - "properties": { - "version": { - "type": "string", - "enum": [ - "0.3" - ] - }, - "axes": { - "type": "array", - "minItems": 2, - "items": { - "type": "string", - "pattern": "^[xyzct]$" - } - } - }, - "required": [ - "axes" + }, + "version": { + "type": "string", + "enum": [ + "0.1" ] }, - { + "metadata": { "type": "object", "properties": { + "method": { + "type": "string" + }, "version": { - "type": "string", - "enum": [ - "0.2" - ] + "type": "string" } } } - ], + }, "required": [ - "name", "datasets" + "datasets" ] }, "minItems": 1, diff --git a/0.1/schemas/plate.schema b/0.1/schemas/plate.schema new file mode 100644 index 00000000..09110cfc --- /dev/null +++ b/0.1/schemas/plate.schema @@ -0,0 +1,112 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:8000/plate.schema", + "title": "NGFF Plate", + "description": "JSON from OME-NGFF Plate .zattrs", + "type": "object", + "properties": { + "plate": { + "type": "object", + "properties": { + "version": { + "type": "string", + "enum": [ + "0.1" + ] + }, + "name": { + "type": "string" + }, + "columns": { + "description": "Columns of the Plate grid", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "minItems": 1, + "uniqueItems": true + }, + "rows": { + "description": "Rows of the Plate grid", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "minItems": 1, + "uniqueItems": true + }, + "wells": { + "description": "Rows of the Plate grid", + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": [ + "path" + ] + }, + "minItems": 1, + "uniqueItems": true + }, + "field_count": { + "description": "Maximum number of fields per view across all wells." + }, + "acquisitions": { + "description": "Rows of the Plate grid", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "maximumfieldcount": { + "type": "number" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "starttime": { + "type": "number" + } + }, + "required": [ + "id" + ] + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "version", "columns", "rows", "wells" + ] + } + }, + "required": [ + "plate" + ] +} \ No newline at end of file diff --git a/0.1/schemas/strict_image.schema b/0.1/schemas/strict_image.schema new file mode 100644 index 00000000..9c573d07 --- /dev/null +++ b/0.1/schemas/strict_image.schema @@ -0,0 +1,18 @@ +{ + "allOf": [ + { + "$ref": "https://ngff.openmicroscopy.org/0.1/schemas/image.schema" + }, + { + "properties": { + "multiscales": { + "items": { + "required": [ + "version", "metadata", "type", "name" + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/0.1/tests/test_validation.py b/0.1/tests/test_validation.py new file mode 100644 index 00000000..a6bde4d3 --- /dev/null +++ b/0.1/tests/test_validation.py @@ -0,0 +1,95 @@ +import json +import os +import glob + +import pytest + +from jsonschema import validate, RefResolver +from jsonschema.validators import validator_for +from jsonschema.exceptions import ValidationError + + +IMAGE_SCHEMA_KEY = "https://ngff.openmicroscopy.org/0.1/schemas/image.schema" + + +def files(): + return list(glob.glob(f"examples/*/valid/*.json")) + \ + list(glob.glob(f"examples/*/invalid/*.json")) + + +def strict(): + return list(glob.glob(f"examples/image/valid/*.json")) + + +def ids(paths): + return [str(x).split("/")[-1][0:-5] for x in paths] + + +@pytest.mark.parametrize("testfile", files(), ids=ids(files())) +def test_json(testfile): + + test_json, schema = load_instance_and_schema(testfile) + + if "invalid" in testfile: + with pytest.raises(ValidationError): + validate(instance=test_json, schema=schema) + else: + validate(instance=test_json, schema=schema) + + +class LocalRefResolver(RefResolver): + + def resolve_remote(self, url): + # Use remote URL to generate local path + url = url.replace("https://ngff.openmicroscopy.org/0.1/", "") + # Load local document and cache it + document = load_json(url) + self.store[url] = document + return document + + +@pytest.mark.parametrize("testfile", strict(), ids=ids(strict())) +def test_strict_rules(testfile): + + test_json, schema = load_instance_and_schema(testfile, strict=True) + + # Check for all validation errors without throwing exception + cls = validator_for(schema) + cls.check_schema(schema) + + # Use our local resolver subclass to resolve local documents + localResolver = LocalRefResolver.from_schema(schema) + validator = cls(schema, localResolver) + + warnings = list(validator.iter_errors(test_json)) + for warning in warnings: + print("WARNING", warning.message) + # ONLY the complete example has no warnings in strict mode + if "complete" not in testfile: + assert len(warnings) > 0 + + +def load_instance_and_schema(path, strict=False): + # Load the correct schema + test_json = load_json(path) + # we don't have @type in this version + if "multiscales" in test_json: + schema_name = "image.schema" + elif "plate" in test_json: + schema_name = "plate.schema" + else: + raise Exception("No schema found") + + schema = load_json('schemas/' + schema_name) + + if strict and schema_name == "image.schema": + strict_path = 'schemas/strict_' + schema_name + schema = load_json(strict_path) + + return (test_json, schema) + + +def load_json(path): + with open(path) as f: + json_data = json.loads(f.read()) + return json_data diff --git a/0.1/tox.ini b/0.1/tox.ini new file mode 100644 index 00000000..2de32a96 --- /dev/null +++ b/0.1/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = v01 +skipsdist = True + +[testenv] +deps = + pytest + jsonschema +commands = + pytest tests --color=yes --basetemp={envtmpdir} {posargs:-v} diff --git a/0.2/examples/invalid/invalid_channels_color.json b/0.2/examples/invalid/invalid_channels_color.json new file mode 100644 index 00000000..56f9f7f2 --- /dev/null +++ b/0.2/examples/invalid/invalid_channels_color.json @@ -0,0 +1,31 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ] + } + ], + "omero": { + "channels": [ + { + "active": true, + "coefficient": 1.0, + "color": 255, + "family": "linear", + "label": "1234", + "window": { + "end": 1765.0, + "max": 2555.0, + "min": 5.0, + "start": 0.0 + } + } + ] + } +} diff --git a/0.2/examples/invalid/invalid_channels_window.json b/0.2/examples/invalid/invalid_channels_window.json new file mode 100644 index 00000000..534b6eb6 --- /dev/null +++ b/0.2/examples/invalid/invalid_channels_window.json @@ -0,0 +1,31 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ] + } + ], + "omero": { + "channels": [ + { + "active": true, + "coefficient": 1.0, + "color": "ff0000", + "family": "linear", + "label": "1234", + "window": { + "end": "100", + "max": 2555.0, + "min": 5.0, + "start": 0.0 + } + } + ] + } +} diff --git a/0.2/examples/invalid/invalid_path.json b/0.2/examples/invalid/invalid_path.json new file mode 100644 index 00000000..0ea1f2d9 --- /dev/null +++ b/0.2/examples/invalid/invalid_path.json @@ -0,0 +1,17 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + }, + { + "path": 0 + } + ] + } + ] +} diff --git a/0.2/examples/invalid/missing_datasets.json b/0.2/examples/invalid/missing_datasets.json new file mode 100644 index 00000000..6604968d --- /dev/null +++ b/0.2/examples/invalid/missing_datasets.json @@ -0,0 +1,9 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example" + } + ] +} diff --git a/0.2/examples/invalid/missing_path.json b/0.2/examples/invalid/missing_path.json new file mode 100644 index 00000000..487a4266 --- /dev/null +++ b/0.2/examples/invalid/missing_path.json @@ -0,0 +1,17 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example", + "datasets": [ + { + "foo": "path/to/0" + }, + { + "path": "1" + } + ] + } + ] +} diff --git a/0.2/examples/invalid/no_datasets.json b/0.2/examples/invalid/no_datasets.json new file mode 100644 index 00000000..839a60aa --- /dev/null +++ b/0.2/examples/invalid/no_datasets.json @@ -0,0 +1,15 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example", + "datasets": [], + "axes": [ + "z", + "y", + "x" + ] + } + ] +} diff --git a/0.2/examples/invalid/no_multiscales.json b/0.2/examples/invalid/no_multiscales.json new file mode 100644 index 00000000..d6dbfb33 --- /dev/null +++ b/0.2/examples/invalid/no_multiscales.json @@ -0,0 +1,4 @@ +{ + "@type": "ngff:Image", + "multiscales": [] +} diff --git a/0.2/examples/valid/image.json b/0.2/examples/valid/image.json new file mode 100644 index 00000000..2c1e7d27 --- /dev/null +++ b/0.2/examples/valid/image.json @@ -0,0 +1,14 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "name": "example", + "datasets": [ + {"path": "path/to/0"}, + {"path": "1"}, + {"path": "2"} + ] + } + ] +} diff --git a/0.2/examples/valid/image_metadata.json b/0.2/examples/valid/image_metadata.json new file mode 100644 index 00000000..3ca5cd47 --- /dev/null +++ b/0.2/examples/valid/image_metadata.json @@ -0,0 +1,28 @@ +{ + "@id": "top", + "@type": "ngff:Image", + "multiscales": [ + { + "@id": "inner", + "version": "0.2", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + } + ], + "type": "gaussian", + "metadata": { + "method": "skimage.transform.pyramid_gaussian", + "version": "0.16.1", + "args": [ + "true", + "false" + ], + "kwargs": { + "multichannel": true + } + } + } + ] +} diff --git a/0.2/examples/valid/image_omero.json b/0.2/examples/valid/image_omero.json new file mode 100644 index 00000000..20b3b892 --- /dev/null +++ b/0.2/examples/valid/image_omero.json @@ -0,0 +1,58 @@ +{ + "@id": "#my-image", + "@type": "ngff:Image", + "multiscales": [ + { + "@id": "#my-pyramid", + "version": "0.2", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + }, + { + "path": "1" + }, + { + "path": "2" + } + ], + "type": "gaussian", + "metadata": { + "method": "skimage.transform.pyramid_gaussian", + "version": "0.16.1", + "args": [ + "true", + "false" + ], + "kwargs": { + "multichannel": true + } + } + } + ], + "omero": { + "id": 1, + "version": "0.2", + "channels": [ + { + "active": true, + "color": "0000FF", + "family": "linear", + "inverted": false, + "label": "1234", + "window": { + "end": 1765.0, + "max": 2555.0, + "min": 5.0, + "start": 0.0 + } + } + ], + "rdefs": { + "defaultZ": 0, + "defaultT": 0, + "model": "color" + } + } +} diff --git a/0.2/examples/valid/missing_name.json b/0.2/examples/valid/missing_name.json new file mode 100644 index 00000000..3942a543 --- /dev/null +++ b/0.2/examples/valid/missing_name.json @@ -0,0 +1,13 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "0.2", + "datasets": [ + { + "path": "path/to/0" + } + ] + } + ] +} diff --git a/0.3/examples/valid/image_v0.2.json b/0.2/examples/valid/missing_version.json similarity index 87% rename from 0.3/examples/valid/image_v0.2.json rename to 0.2/examples/valid/missing_version.json index ab73ed40..c278ddef 100644 --- a/0.3/examples/valid/image_v0.2.json +++ b/0.2/examples/valid/missing_version.json @@ -2,7 +2,6 @@ "@type": "ngff:Image", "multiscales": [ { - "version": "0.2", "name": "example", "datasets": [ { @@ -11,4 +10,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/0.2/schemas/image.schema b/0.2/schemas/image.schema new file mode 100644 index 00000000..6eb26274 --- /dev/null +++ b/0.2/schemas/image.schema @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:8000/image.schema", + "title": "NGFF Image", + "description": "JSON from OME-NGFF .zattrs", + "type": "object", + "properties": { + "multiscales": { + "description": "The multiscale datasets for this image", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "datasets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"] + } + }, + "version": { + "type": "string", + "enum": [ + "0.2" + ] + } + }, + "required": [ + "datasets" + ] + }, + "minItems": 1, + "uniqueItems": true + }, + "omero": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "window": { + "type": "object", + "properties": { + "end": { + "type": "number" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + }, + "start": { + "type": "number" + } + }, + "required": [ + "start", + "min", + "end", + "max" + ] + }, + "label": { + "type": "string" + }, + "family": { + "type": "string" + }, + "color": { + "type": "string" + }, + "active": { + "type": "boolean" + } + }, + "required": [ + "window", + "color" + ] + } + } + }, + "required": [ + "channels" + ] + } + }, + "required": [ "multiscales" ] +} diff --git a/0.2/tests/test_validation.py b/0.2/tests/test_validation.py new file mode 100644 index 00000000..48a3c4ed --- /dev/null +++ b/0.2/tests/test_validation.py @@ -0,0 +1,40 @@ +import json +import glob + +import pytest + +from jsonschema import validate +from jsonschema.exceptions import ValidationError + + +def files(): + return list(glob.glob(f"examples/valid/*.json")) + \ + list(glob.glob(f"examples/invalid/*.json")) + +def ids(): + return [str(x).split("/")[-1][0:-5] for x in files()] + + +@pytest.mark.parametrize("testfile", files(), ids=ids()) +def test_json(testfile): + + if "invalid" in testfile: + with pytest.raises(ValidationError): + json_schema(testfile) + else: + json_schema(testfile) + + +def json_schema(path): + # Load the correct schema + with open(path) as f: + test_json = json.loads(f.read()) + # we don't have @type in this version + if "multiscales" in test_json: + schema_name = "image.schema" + else: + raise Exception("No schema found") + + with open('schemas/' + schema_name) as f: + schema = json.loads(f.read()) + validate(instance=test_json, schema=schema) diff --git a/0.2/tox.ini b/0.2/tox.ini new file mode 100644 index 00000000..5f9175d3 --- /dev/null +++ b/0.2/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = v02 +skipsdist = True + +[testenv] +deps = + pytest + jsonschema +commands = + pytest tests --color=yes --basetemp={envtmpdir} {posargs:-v} diff --git a/0.3/examples/invalid/invalid_version.json b/0.3/examples/invalid/invalid_version.json new file mode 100644 index 00000000..ba995783 --- /dev/null +++ b/0.3/examples/invalid/invalid_version.json @@ -0,0 +1,25 @@ +{ + "@type": "ngff:Image", + "multiscales": [ + { + "version": "invalid", + "name": "example", + "datasets": [ + { + "path": "path/to/0" + }, + { + "path": "1" + }, + { + "path": "2" + } + ], + "axes": [ + "z", + "y", + "x" + ] + } + ] +} \ No newline at end of file diff --git a/0.3/examples/valid/image.json b/0.3/examples/valid/image.json index 98f32e79..ebcb882d 100644 --- a/0.3/examples/valid/image.json +++ b/0.3/examples/valid/image.json @@ -2,8 +2,6 @@ "@type": "ngff:Image", "multiscales": [ { - "version": "0.3", - "name": "example", "datasets": [ {"path": "path/to/0"}, {"path": "1"}, diff --git a/0.3/examples/invalid/missing_name.json b/0.3/examples/valid/missing_name.json similarity index 91% rename from 0.3/examples/invalid/missing_name.json rename to 0.3/examples/valid/missing_name.json index a0b0963f..22697e44 100644 --- a/0.3/examples/invalid/missing_name.json +++ b/0.3/examples/valid/missing_name.json @@ -6,9 +6,6 @@ "datasets": [ { "path": "path/to/0" - }, - { - "path": 0 } ], "type": "gaussian", diff --git a/0.3/examples/invalid/missing_version.json b/0.3/examples/valid/missing_version.json similarity index 100% rename from 0.3/examples/invalid/missing_version.json rename to 0.3/examples/valid/missing_version.json diff --git a/0.3/schemas/image.schema b/0.3/schemas/image.schema new file mode 100644 index 00000000..afc2bd16 --- /dev/null +++ b/0.3/schemas/image.schema @@ -0,0 +1,109 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ngff.openmicroscopy.org/0.3/schemas/image.schema", + "title": "NGFF Image", + "description": "JSON from OME-NGFF .zattrs", + "type": "object", + "properties": { + "multiscales": { + "description": "The multiscale datasets for this image", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "datasets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + }, + "required": ["path"] + } + }, + "version": { + "type": "string", + "enum": [ + "0.3" + ] + }, + "axes": { + "type": "array", + "minItems": 2, + "items": { + "type": "string", + "pattern": "^[xyzct]$" + } + } + }, + "required": [ + "datasets", "axes" + ] + }, + "minItems": 1, + "uniqueItems": true + }, + "omero": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "window": { + "type": "object", + "properties": { + "end": { + "type": "number" + }, + "max": { + "type": "number" + }, + "min": { + "type": "number" + }, + "start": { + "type": "number" + } + }, + "required": [ + "start", + "min", + "end", + "max" + ] + }, + "label": { + "type": "string" + }, + "family": { + "type": "string" + }, + "color": { + "type": "string" + }, + "active": { + "type": "boolean" + } + }, + "required": [ + "window", + "color" + ] + } + } + }, + "required": [ + "channels" + ] + } + }, + "required": [ "multiscales" ] +} diff --git a/0.3/schemas/jsonld/shacl.ttl b/0.3/schemas/jsonld/shacl.ttl index 7bc00fd9..02cb213b 100644 --- a/0.3/schemas/jsonld/shacl.ttl +++ b/0.3/schemas/jsonld/shacl.ttl @@ -56,14 +56,12 @@ schema:Multiscale_Shape a sh:NodeShape; sh:property [ sh:path ngff:name; sh:datatype xsd:string; - sh:minCount 1; ]; sh:property [ sh:path ngff:version; sh:datatype xsd:string; - sh:minCount 1; - sh:hasValue "0.3"; + sh:pattern "0.3"; ]; sh:property [ diff --git a/0.3/schemas/strict_image.schema b/0.3/schemas/strict_image.schema new file mode 100644 index 00000000..d3758444 --- /dev/null +++ b/0.3/schemas/strict_image.schema @@ -0,0 +1,18 @@ +{ + "allOf": [ + { + "$ref": "https://ngff.openmicroscopy.org/0.3/schemas/image.schema" + }, + { + "properties": { + "multiscales": { + "items": { + "required": [ + "version", "metadata", "type", "name" + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/0.3/tests/test_validation.py b/0.3/tests/test_validation.py index 30c0af97..899f062f 100644 --- a/0.3/tests/test_validation.py +++ b/0.3/tests/test_validation.py @@ -15,12 +15,6 @@ from schema_salad.exceptions import ValidationException as SaladErr -# NB: Need to have contents of /json_schema served at localhost:8000 -# so that http://localhost:8000/omero.schema.json is valid - -# cd json_schema -# python -m http.server - class LDErr(Exception): pass @@ -34,7 +28,7 @@ def test_json(method, testfile, httpserver): for uri, filename in ( ("/context.json", "schemas/jsonld/context.json"), - ("/image.schema", "schemas/json_schema/image.schema"), + ("/image.schema", "schemas/image.schema"), ): with open(filename) as o: httpserver.expect_request(uri).respond_with_data(o.read()) @@ -46,8 +40,6 @@ def test_json(method, testfile, httpserver): pytest.skip("not supported") else: - if "jsonld" in method.__name__ and "v0.2" in testfile: - pytest.skip("not supported") method(testfile) @@ -69,7 +61,7 @@ def method(request): if request.param == "jsonschema": - with open('schemas/json_schema/image.schema') as f: + with open('schemas/image.schema') as f: schema = json.loads(f.read()) def json_schema(path):