diff --git a/tools/schemacode/pyproject.toml b/tools/schemacode/pyproject.toml index 08affa2f79..9a055a1493 100644 --- a/tools/schemacode/pyproject.toml +++ b/tools/schemacode/pyproject.toml @@ -1,5 +1,9 @@ [build-system] -requires = ["pdm-backend", "acres", "pyyaml"] +requires = [ + "pdm-backend", + "acres", + "pyyaml", +] build-backend = "pdm.backend" [project] @@ -52,6 +56,7 @@ tests = [ "coverage[toml]", "pytest>6", "pytest-cov", + "requests", ] all = [ "bidsschematools[tests]", diff --git a/tools/schemacode/src/bidsschematools/schema.py b/tools/schemacode/src/bidsschematools/schema.py index c1055658c3..bbf79305af 100644 --- a/tools/schemacode/src/bidsschematools/schema.py +++ b/tools/schemacode/src/bidsschematools/schema.py @@ -9,6 +9,9 @@ from copy import deepcopy from functools import cache, lru_cache from pathlib import Path +import subprocess +import urllib3 +from tempfile import TemporaryDirectory from . import _lazytypes as lt from . import data, utils @@ -204,7 +207,9 @@ def flatten_enums(namespace: Namespace, inplace=True) -> Namespace: @lru_cache -def load_schema(schema_path: lt.Traversable | str | None = None) -> Namespace: +def load_schema( + schema_path: lt.Traversable | str | None = None, bids_version: str = None +) -> Namespace: """Load the schema into a dict-like structure. This function allows the schema, like BIDS itself, to be specified in @@ -218,6 +223,9 @@ def load_schema(schema_path: lt.Traversable | str | None = None) -> Namespace: schema_path : str, optional Directory containing yaml files or yaml file. If ``None``, use the default schema packaged with ``bidsschematools``. + bids_version: str, optional + Version of bids release to load schema from, allows user to have full namespace/dict + like access to bids schema given a specific version Returns ------- @@ -228,7 +236,7 @@ def load_schema(schema_path: lt.Traversable | str | None = None) -> Namespace: ----- This function is cached, so it will only be called once per schema path. """ - if schema_path is None: + if schema_path is None and bids_version is None: # Default to bundled JSON, fall back to bundled YAML directory schema_path = data.load.readable("schema.json") if not schema_path.is_file(): @@ -239,8 +247,33 @@ def load_schema(schema_path: lt.Traversable | str | None = None) -> Namespace: assert isinstance(schema_path, Path) schema_path = Path.resolve(schema_path.parent / content) lgr.info("No schema path specified, defaulting to the bundled schema, `%s`.", schema_path) - elif isinstance(schema_path, str): + elif isinstance(schema_path, str) and bids_version is None: schema_path = Path(schema_path) + elif schema_path is None and bids_version: + if re.search(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$", bids_version): + schema_url = "https://bids-specification.readthedocs.io/en/v{}/schema.json".format(bids_version) + + elif bids_version in ("stable", "latest"): + schema_url = "https://bids-specification.readthedocs.io/en/{}/schema.json".format(bids_version) + else: + raise Exception( + NameError( + f"BIDS version is limited to `stable`, `latest`, or semantic format `X.X.X`, you gave {bids_version}" + ) + ) + http = urllib3.PoolManager() + response = http.request("GET", schema_url) + if response.status != 200: + raise urllib3.exceptions.HTTPError( + f"Unable to retrieve schema from {schema_url} (status {response.status})" + ) + else: + with TemporaryDirectory() as tmp: + schema_path = Path(tmp) / "schema.json" + with open(schema_path, 'w') as f: + json.dump(response.json(), f) + + return Namespace.from_json(schema_path.read_text()) # JSON file: just load it if schema_path.is_file(): diff --git a/tools/schemacode/src/bidsschematools/tests/test_schema.py b/tools/schemacode/src/bidsschematools/tests/test_schema.py index 0663cec898..984c805d4e 100644 --- a/tools/schemacode/src/bidsschematools/tests/test_schema.py +++ b/tools/schemacode/src/bidsschematools/tests/test_schema.py @@ -3,8 +3,10 @@ import json import os import subprocess +import requests from collections.abc import Mapping + import pytest from jsonschema.exceptions import ValidationError @@ -47,6 +49,24 @@ def test_load_schema(schema_dir): assert "$ref" not in str(schema_obj) +@pytest.mark.skipif( + requests.get("https://bids-specification.readthedocs.io/").status_code != 200, + reason="Unable to reach 'https://bids-specification.readthedocs.io, skipping url retrieval", +) +def test_load_schema_from_url(): + # load using latest and stable keywords + assert isinstance(schema.load_schema(bids_version="latest"), Mapping) + assert isinstance(schema.load_schema(bids_version="stable"), Mapping) + + # load with known version + assert isinstance(schema.load_schema(bids_version="1.11.1"), Mapping) + + # load with bogus url + #bad_url = "https://bids-specification.readthedocs.io/en/not-a-real-version/schema.json" + #with pytest.raises(Exception): + # schema.load_schema(bad_url) + + def test_object_definitions(schema_obj): """Ensure that object definitions in the schema contain required fields.""" for obj_type, obj_type_def in schema_obj["objects"].items():