Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/node_registry.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
"service-wms",
"service-wfs"
],
"types": [
"data",
"wps",
"wms",
"wfs"
],
"description": "GeoServer is a server that allows users to view and edit geospatial data.",
"links": [
{
Expand All @@ -60,6 +66,9 @@
"keywords": [
"service-ogcapi_processes"
],
"types": [
"ogcapi_processes"
],
"description": "An OGC-API flavored Execution Management Service",
"links": [
{
Expand Down
46 changes: 46 additions & 0 deletions marble_node_registry/migrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Migrations are used to ensure that data provided by nodes that are using an
# older version of the schema are updated automatically to comply with the newest
# version of the schema.
#
# If a backwards incompatible change is introduced in the schema, please make a
# new migration function here to ensure that older data is properly updated.
#
# Migration functions will be applied to each node's data in the order that they
# appear in the MIGRATIONS variable at the bottom of this file.
#
# All migration function should take a single argument which contains the node's
# data and modify that data in place.

def convert_keywords_to_types(data: dict) -> None:
"""
Add service types if they don't exist.

Since version 1.3.0 service "types" are now required. If they don't exist
then they can be derived from service "keywords" which were used in place
of "types" prior to this version.
"""

keyword2type = {
"catalog": "catalog",
"data": "data",
"jupyterhub": "jupyterhub",
"other": "other",
"service-wps": "wps",
"service-wms": "wms",
"service-wfs": "wfs",
"service-wcs": "wcs",
"service-ogcapi_processes": "ogcapi_processes"
}
for service in data["services"]:
if "types" not in service:
service["types"] = []
for keyword in service["keywords"]:
if (type_ := keyword2type.get(keyword)):
service["types"].append(type_)
if not service["types"]:
service["types"].append("other")


MIGRATIONS = (
convert_keywords_to_types,
)
Comment on lines +44 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this migration be within a version mapping?

For example, migrating from 1.2->1.3 makes senses to apply this fix, but it shouldn't be done if 1.3 is already applied. Maybe future migration will require specific version checks as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about that but then I decided against it due to the complexity of tracking version differences within the schema.

If you'd prefer that strategy I can make it work but it didn't seem worth it at the time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the current approach if it avoids unnecessary complexity/over-engineering since I don't expect the schemas to change too often. If they change drastically, it would probably be a major version anyway, and migration would not be possible if such change was actually needed.

11 changes: 11 additions & 0 deletions marble_node_registry/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import datetime
from copy import deepcopy

from migrations import MIGRATIONS

THIS_DIR = os.path.dirname(__file__)
ROOT_DIR = os.path.dirname(THIS_DIR)
SCHEMA_FILE = os.path.join(ROOT_DIR, "node_registry.schema.json")
Expand Down Expand Up @@ -85,6 +87,15 @@ def update_registry() -> None:
)
continue

try:
for migration in MIGRATIONS:
migration(data)
except Exception as e:
registry[name] = org_data
registry[name]["status"] = "invalid_configuration"
sys.stderr.write(f"unable to apply migrations for Node named {name}: {e}.")
continue

try:
jsonschema.validate(instance=registry, schema=schema)
except jsonschema.exceptions.ValidationError as e:
Expand Down
30 changes: 28 additions & 2 deletions node_registry.schema.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add the explicit $id as https://raw.githubusercontent.com/DACCS-Climate/Marble-node-registry/refs/tags/1.2.0/node_registry.schema.json (and after bump/tag with 1.3.0)? That would help ensure the reported $schema by the services matches the specific versions that exist.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/DACCS-Climate/Marble-node-registry/refs/tags/1.2.0/node_registry.schema.json",
"patternProperties": {
"^[a-zA-Z0-9]+$": {
"type": "object",
Expand Down Expand Up @@ -150,6 +151,7 @@
"type": "object",
"required": [
"name",
"types",
"keywords",
"description",
"links"
Expand All @@ -159,12 +161,36 @@
"type": "string",
"minLength": 1
},
"keywords": {
"types": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^catalog|data|jupyterhub|other|service-(wps|wms|wfs|wcs|ogcapi_processes)$"
"enum": [
"auth",
"management",
"catalog",
"stac",
"data",
"jupyterhub",
"other",
"wps",
"wms",
"wfs",
"wcs",
"ogcapi_processes",
"ogcapi_dggs",
"ogcapi_coverages",
"ogcapi_features",
"ogcapi_records"
]
}
},
"keywords": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"description": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
addopts = [
"--import-mode=importlib",
]
pythonpath = "."
pythonpath = "./marble_node_registry"
1 change: 1 addition & 0 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def registry_content_with_services(registry_content):
{
"name": "test-service",
"keywords": ["other"],
"types": ["other"],
"description": "test service",
"version": "1.2.3",
"links": [
Expand Down
37 changes: 29 additions & 8 deletions tests/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import jsonschema
import pytest

from marble_node_registry import update
import update # type: ignore

GOOD_SERVICES = {
"services": [
{
"name": "geoserver",
"types": ["data", "wps", "wms", "wfs"],
"keywords": ["data", "service-wps", "service-wms", "service-wfs"],
"description": "GeoServer is a server that allows users to view and edit geospatial data.",
"links": [
Expand All @@ -20,7 +21,8 @@
},
{
"name": "weaver",
"keywords": ["service-ogcapi_processes"],
"types": ["ogcapi_processes"],
"keywords": ["service-ogcapi_processes", "some-other-keyword"],
"description": "An OGC-API flavored Execution Management Service",
"links": [
{"rel": "service", "type": "application/json", "href": "https://daccs-uoft.example.com/weaver/"},
Expand Down Expand Up @@ -265,7 +267,7 @@ def test_last_updated_updated(self, example_node_name, example_registry_content,
example_node_name
].get("last_updated")

def test_final_registry_valid(self, updated_registry, node_registry_schema):
def test_final_registry_validity(self, updated_registry, node_registry_schema):
jsonschema.validate(instance=updated_registry.call_args.args[0], schema=node_registry_schema)


Expand Down Expand Up @@ -300,7 +302,7 @@ def test_last_updated_no_change(self, example_node_name, example_registry_conten
example_node_name
].get("last_updated")

def test_final_registry_valid(self, updated_registry, node_registry_schema):
def test_final_registry_validity(self, updated_registry, node_registry_schema):
jsonschema.validate(instance=updated_registry.call_args.args[0], schema=node_registry_schema)


Expand Down Expand Up @@ -350,11 +352,30 @@ class TestOnlineNodeUpdateWithInvalidServices(InvalidResponseTests, NonInitialTe
services = {"services": [{"bad_key": "some_value"}]}


class TestOnlineNodeUpdateWithInvalidServiceKeywords(InvalidResponseTests, NonInitialTests):
"""Test when updates have previously been run and the reported services keywords are not valid"""
class TestOnlineNodeUpdateWithInvalidServiceTypes(InvalidResponseTests, NonInitialTests):
"""Test when updates have previously been run and the reported services types are not valid"""

services = deepcopy(GOOD_SERVICES)

@pytest.fixture(scope="class", autouse=True)
def bad_keywords(self):
self.services["services"][0]["keywords"] = ["something-bad"]
def bad_types(self):
self.services["services"][0]["types"] = ["something-bad"]


class TestOnlineNodeUpdateWithNoTypes(ValidResponseTests, NonInitialTests):
"""
Test when updates have previously been run and there are no services types

This ensures that service types are updated as expected from the provided keywords
"""

services = deepcopy(GOOD_SERVICES)

@pytest.fixture(scope="class", autouse=True)
def no_types(self):
for service in self.services["services"]:
service.pop("types")

def test_services_updated(self, example_node_name, updated_registry):
"""Test that the services values are updated"""
assert updated_registry.call_args.args[0][example_node_name]["services"] == GOOD_SERVICES["services"]