Skip to content
Draft

xrlint #1231

Show file tree
Hide file tree
Changes from all 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
61 changes: 61 additions & 0 deletions compliance_checker/plugins/acdd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from xrlint.plugin import Plugin
from xrlint.util.importutil import import_submodules

rules_1_0 = {
"1.0_attrs_highly_recommended": "error",
"1.0_attrs_recommended": "warn",
"1.0_attrs_suggested": "warn",
}

rules_1_1 = {
"1.1_attrs_highly_recommended": "error",
"1.1_attrs_recommended": "warn",
"1.1_attrs_suggested": "warn",
}

rules_1_3 = {
"1.3_conventions": "error",
"1.3_attrs_highly_recommended": "error",
"1.3_attrs_recommended": "warn",
"1.3_attrs_suggested": "warn",
"1.3_no_blanks_in_id": "warn",
"1.3_metadata_link": "warn",
"1.3_dates_iso_format": "warn",
}


def export_plugin() -> Plugin:
from .plugin import plugin

import_submodules("compliance_checker.plugins.acdd.rules")

plugin.define_config("recommended", {"name": "recommended", "rules": rules_1_3})

plugin.define_config("acdd_1.3", {"name": "ACDD 1.3", "rules": rules_1_3})

plugin.define_config(
"acdd_1.3_strict_reccomended",
{
"name": "ACDD 1.3 (strict recommended)",
"rules": {
**rules_1_3,
"1.3_attrs_recommended": "error",
},
},
)

plugin.define_config(
"acdd_1.3_strict",
{"name": "ACDD 1.3 (strict)", "rules": dict.fromkeys(rules_1_3, "error")},
)

plugin.define_config(
"acdd_1.3_warn",
{"name": "ACDD 1.3 (as warnings)", "rules": dict.fromkeys(rules_1_3, "warn")},
)

plugin.define_config("acdd_1.1", {"name": "ACDD 1.1", "rules": rules_1_1})

plugin.define_config("acdd_1.0", {"name": "ACDD 1.0", "rules": rules_1_0})

return plugin
7 changes: 7 additions & 0 deletions compliance_checker/plugins/acdd/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from xrlint.plugin import new_plugin

plugin = new_plugin(
"accd_1.3",
"1.3",
docs_url="https://wiki.esipfed.org/Attribute_Convention_for_Data_Discovery_1-3",
)
166 changes: 166 additions & 0 deletions compliance_checker/plugins/acdd/rules/attributes.py

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions compliance_checker/plugins/acdd/rules/conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from xrlint.node import DatasetNode
from xrlint.rule import RuleContext, RuleOp

from compliance_checker.plugins.acdd.plugin import plugin


@plugin.define_rule(
"1.3_conventions",
version="1.3",
docs_url="https://wiki.esipfed.org/Attribute_Convention_for_Data_Discovery_1-3",
)
class Conventions(RuleOp):
def validate_dataset(self, ctx: RuleContext, node: DatasetNode):
if "Conventions" not in node.dataset.attrs:
ctx.report(
"Missing attribute 'Conventions'.",
suggestions=[
"Include 'Conventions' with 'ACDD-1.3' attribute in the dataset.",
],
)
else:
value = node.dataset.attrs.get("Conventions")
if not isinstance(value, str) and value:
ctx.report(f"Invalid attribute 'Conventions': {value!r}")
elif "ACDD-1.3" not in value:
ctx.report(
f"Attribute 'Conventions' needs to contain 'ACDD-1.3' in addition to the current: {value!r}",
suggestions=["Include 'ACDD-1.3' in the 'Conventions' attribute."],
)
31 changes: 31 additions & 0 deletions compliance_checker/plugins/acdd/rules/iso_dates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from xrlint.node import DatasetNode
from xrlint.rule import RuleContext, RuleOp

from compliance_checker.plugins.acdd.plugin import plugin
from compliance_checker.util import datetime_is_iso


@plugin.define_rule(
"1.3_dates_iso_format",
version="1.3",
docs_url="https://wiki.esipfed.org/Attribute_Convention_for_Data_Discovery_1-3",
)
class IsoDates(RuleOp):
def validate_dataset(self, ctx: RuleContext, node: DatasetNode):
for attr in (
"date_created",
"date_issued",
"date_modified",
"date_metadata_modified",
):
if attr in node.dataset.attrs:
value = node.dataset.attrs[attr]
iso_check, _msg = datetime_is_iso(value)

if not iso_check:
ctx.report(
f"Attribute '{attr}' is not in ISO format: {value!r}",
suggestions=[
"Change '{attr}' to be in ISO format (e.g. YYYY-MM-DDThh:mm:ssZ).",
],
)
19 changes: 19 additions & 0 deletions compliance_checker/plugins/acdd/rules/metadata_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from xrlint.node import DatasetNode
from xrlint.rule import RuleContext, RuleOp

from compliance_checker.plugins.acdd.plugin import plugin


@plugin.define_rule(
"1.3_metadata_link",
version="1.3",
docs_url="https://wiki.esipfed.org/Attribute_Convention_for_Data_Discovery_1-3",
)
class MetadataLink(RuleOp):
def validate_dataset(self, ctx: RuleContext, node: DatasetNode):
if "metadata_link" in node.dataset.attrs:
value = node.dataset.attrs["metadata_link"]
if "http" not in value:
ctx.report(
f"Metadata URL should include http:// or https://: {value!r}",
)
26 changes: 26 additions & 0 deletions compliance_checker/plugins/acdd/rules/no_id_blanks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from xrlint.node import DatasetNode
from xrlint.rule import RuleContext, RuleOp

from compliance_checker.plugins.acdd.plugin import plugin


@plugin.define_rule(
"1.3_no_blanks_in_id",
version="1.3",
docs_url="https://wiki.esipfed.org/Attribute_Convention_for_Data_Discovery_1-3",
)
class NoBlanksInID(RuleOp):
def validate_dataset(self, ctx: RuleContext, node: DatasetNode):
try:
value = node.dataset.attrs["id"]
except KeyError:
ctx.report(
"Missing attribute 'id'",
suggestions=["Include a non-blank 'id' attribute in the dataset."],
)
return
if " " in value:
ctx.report(
"There should be no blanks in the id field",
suggestions=["There should be no blanks in the id field"],
)
34 changes: 34 additions & 0 deletions compliance_checker/tests/plugins/acdd/rules/test_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import xarray as xr
from xrlint.testing import RuleTest, RuleTester

from compliance_checker.plugins.acdd.rules.attributes import (
Attributes_1_3_Highly_Reccomended,
)

valid_1_3_highly_rec_dataset = xr.Dataset(
attrs={
"title": "Tis only a test",
"summary": "This is only a test dataset.",
"keywords": "test, example, sample",
"conventions": "ACDD-1.3",
},
)
invalid_1_3_highly_rec_dataset = xr.Dataset()


Attributes_1_3_Highly_ReccomendedTest = RuleTester.define_test(
"1.3_attrs_highly_recommended",
Attributes_1_3_Highly_Reccomended,
valid=[RuleTest(dataset=valid_1_3_highly_rec_dataset)],
invalid=[
RuleTest(
dataset=invalid_1_3_highly_rec_dataset,
expected=[
"Missing attribute 'title'",
"Missing attribute 'keywords'",
"Missing attribute 'summary'",
"Missing attribute 'conventions'",
],
),
],
)
35 changes: 35 additions & 0 deletions compliance_checker/tests/plugins/acdd/rules/test_conventions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import xarray as xr
from xrlint.testing import RuleTest, RuleTester

from compliance_checker.plugins.acdd.rules.conventions import Conventions

valid_dataset_0 = xr.Dataset(attrs={"Conventions": "ACDD-1.3"})

invalid_dataset_0 = xr.Dataset()
invalid_dataset_1 = xr.Dataset(attrs={"Conventions": "ACDD-1.2"})
invalid_dataset_2 = xr.Dataset(attrs={"Conventions": 1.3})


ConventionsTest = RuleTester.define_test(
"1.3_conventions",
Conventions,
valid=[
RuleTest(dataset=valid_dataset_0),
],
invalid=[
RuleTest(
dataset=invalid_dataset_0,
expected=["Missing attribute 'Conventions'."],
),
RuleTest(
dataset=invalid_dataset_1,
expected=[
"Attribute 'Conventions' needs to contain 'ACDD-1.3' in addition to the current: 'ACDD-1.2'",
],
),
RuleTest(
dataset=invalid_dataset_2,
expected=["Invalid attribute 'Conventions': 1.3"],
),
],
)
26 changes: 26 additions & 0 deletions compliance_checker/tests/plugins/acdd/rules/test_id_blanks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import xarray as xr
from xrlint.testing import RuleTest, RuleTester

from compliance_checker.plugins.acdd.rules.no_id_blanks import NoBlanksInID

valid_dataset_0 = xr.Dataset(attrs={"id": "testind_dataset"})

invalid_dataset_0 = xr.Dataset()
invalid_dataset_1 = xr.Dataset(attrs={"id": "testing dataset"})


IdBlanksTest = RuleTester.define_test(
"1.3_no_blanks_in_id",
NoBlanksInID,
valid=[RuleTest(dataset=valid_dataset_0)],
invalid=[
RuleTest(
dataset=invalid_dataset_0,
expected=["Missing attribute 'id'"],
),
RuleTest(
dataset=invalid_dataset_1,
expected=["There should be no blanks in the id field"],
),
],
)
61 changes: 61 additions & 0 deletions compliance_checker/tests/plugins/acdd/rules/test_iso_dates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import xarray as xr
from xrlint.testing import RuleTest, RuleTester

from compliance_checker.plugins.acdd.rules.iso_dates import IsoDates

valid_dataset_0 = xr.Dataset(attrs={"date_created": "2023-10-05T12:34:56Z"})
valid_dataset_1 = xr.Dataset(attrs={"date_modified": "2023-10-05"})
valid_dataset_2 = xr.Dataset(attrs={"date_issued": "2023-10-05T12:34:56+00:00"})
valid_dataset_3 = xr.Dataset(
attrs={"date_metadata_modified": "2023-10-05T12:34:56-05:00"},
)
valid_dataset_4 = xr.Dataset() # No date attributes


invalid_dataset_0 = xr.Dataset(attrs={"date_created": "10/05/2023"})
invalid_dataset_1 = xr.Dataset(attrs={"date_issued": "2023-13-05"})
invalid_dataset_2 = xr.Dataset(attrs={"date_modified": "2023-10-05 12:34:56"})
invalid_dataset_3 = xr.Dataset(attrs={"date_metadata_modified": "2023/10/05"})
invalid_dataset_4 = xr.Dataset(
attrs={"date_created": "2023-10-05T25:34:56Z"},
) # Invalid hour

IsoDatesTest = RuleTester.define_test(
"1.3_dates_iso_format",
IsoDates,
valid=[
RuleTest(dataset=valid_dataset_0),
RuleTest(dataset=valid_dataset_1),
RuleTest(dataset=valid_dataset_2),
RuleTest(dataset=valid_dataset_3),
RuleTest(dataset=valid_dataset_4),
],
invalid=[
RuleTest(
dataset=invalid_dataset_0,
expected=["Attribute 'date_created' is not in ISO format: '10/05/2023'"],
),
RuleTest(
dataset=invalid_dataset_1,
expected=["Attribute 'date_issued' is not in ISO format: '2023-13-05'"],
),
RuleTest(
dataset=invalid_dataset_2,
expected=[
"Attribute 'date_modified' is not in ISO format: '2023-10-05 12:34:56'",
],
),
RuleTest(
dataset=invalid_dataset_3,
expected=[
"Attribute 'date_metadata_modified' is not in ISO format: '2023/10/05'",
],
),
RuleTest(
dataset=invalid_dataset_4,
expected=[
"Attribute 'date_created' is not in ISO format: '2023-10-05T25:34:56Z'",
],
),
],
)
28 changes: 28 additions & 0 deletions compliance_checker/tests/plugins/acdd/rules/test_metadata_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import xarray as xr
from xrlint.testing import RuleTest, RuleTester

from compliance_checker.plugins.acdd.rules.metadata_link import MetadataLink

valid_dataset_0 = xr.Dataset(attrs={"metadata_link": "http://example.com/metadata"})
valid_dataset_1 = xr.Dataset(attrs={"metadata_link": "https://example.com/metadata"})
valid_dataset_2 = xr.Dataset()

invalid_dataset_0 = xr.Dataset(attrs={"metadata_link": "example.com/metadata"})

Metadata_LinkTest = RuleTester.define_test(
"1.3_metadata_link",
MetadataLink,
valid=[
RuleTest(dataset=valid_dataset_0),
RuleTest(dataset=valid_dataset_1),
RuleTest(dataset=valid_dataset_2),
],
invalid=[
RuleTest(
dataset=invalid_dataset_0,
expected=[
"Metadata URL should include http:// or https://: 'example.com/metadata'",
],
),
],
)
Loading
Loading