diff --git a/CHANGES.md b/CHANGES.md index a52bb09..5e76412 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,13 @@ ## Version 0.3.0 (in development) -- Added cure rule "flags" +- Added more rules + - core rule "flags" + - core rule "time-coordinate" (#15) + - xcube rule "time-naming" (#15) - Fixed problem where referring to values in modules via - the form `":"` raised. #21 + the form `":"` raised. (#21) - Introduced factory method `new_plugin` which simplifies creating plugin objects. diff --git a/docs/rule-ref.md b/docs/rule-ref.md index 9605915..2a8c46f 100644 --- a/docs/rule-ref.md +++ b/docs/rule-ref.md @@ -36,6 +36,13 @@ Every dataset element should have metadata that describes it. Contained in: `all`-:material-lightning-bolt: `recommended`-:material-alert: +### :material-bug: `time-coordinate` + +Time coordinate (standard_name='time') should have unambiguous time units encoding. +[More information.](https://cfconventions.org/cf-conventions/cf-conventions.html#time-coordinate) + +Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt: + ### :material-lightbulb: `var-units-attr` Every variable should have a valid 'units' attribute. @@ -93,3 +100,10 @@ A single grid mapping shall be used for all spatial data variables of a datacube Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt: +### :material-bug: `time-naming` + +Time coordinate and dimension should be called 'time'. +[More information.](https://xcube.readthedocs.io/en/latest/cubespec.html#temporal-reference) + +Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt: + diff --git a/notebooks/mkdataset.py b/notebooks/mkdataset.py index 01fd8bb..bb64a9b 100644 --- a/notebooks/mkdataset.py +++ b/notebooks/mkdataset.py @@ -19,7 +19,14 @@ def make_dataset() -> xr.Dataset: np.linspace(-90, 90, ny), dims="y", attrs={"units": "degrees"} ), "time": xr.DataArray( - [2010 + y for y in range(nt)], dims="time", attrs={"units": "years"} + [365 * i for i in range(nt)], + dims="time", + attrs={ + "standard_name": "time", + "long_name": "time", + "units": "days since 2020-01-01 utc", + "calendar": "gregorian", + }, ), "spatial_ref": xr.DataArray( 0, @@ -48,6 +55,7 @@ def make_dataset() -> xr.Dataset: def make_dataset_with_issues() -> xr.Dataset: """Create a dataset that produces issues with xrlint core rules.""" invalid_ds = make_dataset() + invalid_ds.time.attrs["units"] = "days since 2020-01-01 ß0:000:00" invalid_ds.attrs = {} invalid_ds.sst.attrs["units"] = 1 invalid_ds["sst_avg"] = xr.DataArray( diff --git a/notebooks/xrlint-linter.ipynb b/notebooks/xrlint-linter.ipynb index 693f719..b8474e7 100644 --- a/notebooks/xrlint-linter.ipynb +++ b/notebooks/xrlint-linter.ipynb @@ -434,41 +434,41 @@ "Coordinates:\n", " * x (x) float64 16B -180.0 180.0\n", " * y (y) float64 24B -90.0 0.0 90.0\n", - " * time (time) int64 32B 2010 2011 2012 2013\n", + " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.1676 0.4534 0.7707 ... 0.9883 0.771\n", - " sst_anomaly (time, y, x) float64 192B 0.7321 0.6003 ... 0.7045 0.7164\n", + " sst (time, y, x) float64 192B 0.3002 0.935 ... 0.2138 0.04308\n", + " sst_anomaly (time, y, x) float64 192B 0.1556 0.3582 ... 0.4877 0.04322\n", "Attributes:\n", - " title: SST-Climatology Subset" + " title: SST-Climatology Subset" ], "text/plain": [ " Size: 464B\n", @@ -476,11 +476,11 @@ "Coordinates:\n", " * x (x) float64 16B -180.0 180.0\n", " * y (y) float64 24B -90.0 0.0 90.0\n", - " * time (time) int64 32B 2010 2011 2012 2013\n", + " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.1676 0.4534 0.7707 ... 0.9883 0.771\n", - " sst_anomaly (time, y, x) float64 192B 0.7321 0.6003 ... 0.7045 0.7164\n", + " sst (time, y, x) float64 192B 0.3002 0.935 ... 0.2138 0.04308\n", + " sst_anomaly (time, y, x) float64 192B 0.1556 0.3582 ... 0.4877 0.04322\n", "Attributes:\n", " title: SST-Climatology Subset" ] @@ -492,7 +492,6 @@ ], "source": [ "from mkdataset import make_dataset\n", - "\n", "ds = make_dataset()\n", "ds" ] @@ -517,7 +516,7 @@ "

<dataset> - ok

\n" ], "text/plain": [ - "Result(config=Config(name=None, files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins={'__core__': Plugin(meta=PluginMeta(name='__core__', version='0.3.0.dev0', ref='xrlint.plugins.core:export_plugin'), rules={'coords-for-dims': Rule(meta=RuleMeta(name='coords-for-dims', version='1.0.0', description='Dimensions of data variables should have corresponding coordinates.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'dataset-title-attr': Rule(meta=RuleMeta(name='dataset-title-attr', version='1.0.0', description='Datasets should be given a non-empty title.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'grid-mappings': Rule(meta=RuleMeta(name='grid-mappings', version='1.0.0', description='Grid mappings, if any, shall have valid grid mapping coordinate variables.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'no-empty-attrs': Rule(meta=RuleMeta(name='no-empty-attrs', version='1.0.0', description='Every dataset element should have metadata that describes it.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'var-units-attr': Rule(meta=RuleMeta(name='var-units-attr', version='1.0.0', description=\"Every variable should have a valid 'units' attribute.\", schema=None, ref=None, docs_url=None, type='suggestion'), op_class=)}, processors={}, configs={'recommended': Config(name='recommended', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), 'all': Config(name='all', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=2, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=2, args=(), kwargs={})}, settings=None)})}, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), file_path='', messages=[], fixable_error_count=0, fixable_warning_count=0, error_count=0, fatal_error_count=0, warning_count=0)" + "Result(config=Config(name=None, files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins={'__core__': Plugin(meta=PluginMeta(name='__core__', version='0.3.0.dev0', ref='xrlint.plugins.core:export_plugin'), rules={'coords-for-dims': Rule(meta=RuleMeta(name='coords-for-dims', version='1.0.0', description='Dimensions of data variables should have corresponding coordinates.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'dataset-title-attr': Rule(meta=RuleMeta(name='dataset-title-attr', version='1.0.0', description='Datasets should be given a non-empty title.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'grid-mappings': Rule(meta=RuleMeta(name='grid-mappings', version='1.0.0', description='Grid mappings, if any, shall have valid grid mapping coordinate variables.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'no-empty-attrs': Rule(meta=RuleMeta(name='no-empty-attrs', version='1.0.0', description='Every dataset element should have metadata that describes it.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'time-coords': Rule(meta=RuleMeta(name='time-coords', version='1.0.0', description=\"Time coordinates (standard_name='time') should have unambiguous time encoding.\", schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'var-units-attr': Rule(meta=RuleMeta(name='var-units-attr', version='1.0.0', description=\"Every variable should have a valid 'units' attribute.\", schema=None, ref=None, docs_url=None, type='suggestion'), op_class=)}, processors={}, configs={'recommended': Config(name='recommended', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), 'all': Config(name='all', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=2, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=2, args=(), kwargs={})}, settings=None)})}, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), file_path='', messages=[], fixable_error_count=0, fixable_warning_count=0, error_count=0, fatal_error_count=0, warning_count=0)" ] }, "execution_count": 5, @@ -912,41 +911,41 @@ "Coordinates:\n", " * x (x) float64 16B -180.0 180.0\n", " * y (y) float64 24B -90.0 0.0 90.0\n", - " * time (time) int64 32B 2010 2011 2012 2013\n", + " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.7409 0.5421 ... 0.09942 0.921\n", - " sst_anomaly (time, y, x) float64 192B 0.2994 0.2066 0.2855 ... 0.304 0.2268\n", - " sst_avg (x, y) float64 48B 0.8309 0.2449 0.4027 0.01275 0.7599 0.4267" + " sst (time, y, x) float64 192B 0.8253 0.09788 ... 0.9227 0.03201\n", + " sst_anomaly (time, y, x) float64 192B 0.1763 0.1965 ... 0.8407 0.6847\n", + " sst_avg (x, y) float64 48B 0.2573 0.7063 0.8764 0.2189 0.7286 0.889" ], "text/plain": [ " Size: 512B\n", @@ -954,12 +953,12 @@ "Coordinates:\n", " * x (x) float64 16B -180.0 180.0\n", " * y (y) float64 24B -90.0 0.0 90.0\n", - " * time (time) int64 32B 2010 2011 2012 2013\n", + " * time (time) int64 32B 0 365 730 1095\n", " spatial_ref int64 8B 0\n", "Data variables:\n", - " sst (time, y, x) float64 192B 0.7409 0.5421 ... 0.09942 0.921\n", - " sst_anomaly (time, y, x) float64 192B 0.2994 0.2066 0.2855 ... 0.304 0.2268\n", - " sst_avg (x, y) float64 48B 0.8309 0.2449 0.4027 0.01275 0.7599 0.4267" + " sst (time, y, x) float64 192B 0.8253 0.09788 ... 0.9227 0.03201\n", + " sst_anomaly (time, y, x) float64 192B 0.1763 0.1965 ... 0.8407 0.6847\n", + " sst_avg (x, y) float64 48B 0.2573 0.7063 0.8764 0.2189 0.7286 0.889" ] }, "execution_count": 6, @@ -985,14 +984,15 @@ "

<dataset>:

\n", "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", "\n", - "
dataset warnMissing 'title' attribute in dataset. dataset-title-attr
dataset.attrs warnMissing metadata, attributes are empty. no-empty-attrs
dataset.data_vars['sst']warnInvalid 'units' attribute in variable 'sst'.var-units-attr
dataset warn Missing 'title' attribute in dataset. dataset-title-attr
dataset.attrs warn Missing metadata, attributes are empty. no-empty-attrs
dataset.coords['time'] errorMissing timezone in 'units' attribute: days since 2020-01-01 ß0:000:00time-coords
dataset.data_vars['sst']warn Invalid 'units' attribute in variable 'sst'. var-units-attr

3 warnings

\n" + "

4 problems (one error and 3 warnings)

\n" ], "text/plain": [ - "Result(config=Config(name=None, files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins={'__core__': Plugin(meta=PluginMeta(name='__core__', version='0.3.0.dev0', ref='xrlint.plugins.core:export_plugin'), rules={'coords-for-dims': Rule(meta=RuleMeta(name='coords-for-dims', version='1.0.0', description='Dimensions of data variables should have corresponding coordinates.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'dataset-title-attr': Rule(meta=RuleMeta(name='dataset-title-attr', version='1.0.0', description='Datasets should be given a non-empty title.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'grid-mappings': Rule(meta=RuleMeta(name='grid-mappings', version='1.0.0', description='Grid mappings, if any, shall have valid grid mapping coordinate variables.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'no-empty-attrs': Rule(meta=RuleMeta(name='no-empty-attrs', version='1.0.0', description='Every dataset element should have metadata that describes it.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'var-units-attr': Rule(meta=RuleMeta(name='var-units-attr', version='1.0.0', description=\"Every variable should have a valid 'units' attribute.\", schema=None, ref=None, docs_url=None, type='suggestion'), op_class=)}, processors={}, configs={'recommended': Config(name='recommended', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), 'all': Config(name='all', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=2, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=2, args=(), kwargs={})}, settings=None)})}, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), file_path='', messages=[Message(message=\"Missing 'title' attribute in dataset.\", node_path='dataset', rule_id='dataset-title-attr', severity=1, fatal=None, fix=None, suggestions=None), Message(message='Missing metadata, attributes are empty.', node_path='dataset.attrs', rule_id='no-empty-attrs', severity=1, fatal=None, fix=None, suggestions=[Suggestion(desc='Make sure to add appropriate metadata attributes to dataset elements.', data=None, fix=None)]), Message(message=\"Invalid 'units' attribute in variable 'sst'.\", node_path=\"dataset.data_vars['sst']\", rule_id='var-units-attr', severity=1, fatal=None, fix=None, suggestions=None)], fixable_error_count=0, fixable_warning_count=0, error_count=0, fatal_error_count=0, warning_count=3)" + "Result(config=Config(name=None, files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins={'__core__': Plugin(meta=PluginMeta(name='__core__', version='0.3.0.dev0', ref='xrlint.plugins.core:export_plugin'), rules={'coords-for-dims': Rule(meta=RuleMeta(name='coords-for-dims', version='1.0.0', description='Dimensions of data variables should have corresponding coordinates.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'dataset-title-attr': Rule(meta=RuleMeta(name='dataset-title-attr', version='1.0.0', description='Datasets should be given a non-empty title.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'grid-mappings': Rule(meta=RuleMeta(name='grid-mappings', version='1.0.0', description='Grid mappings, if any, shall have valid grid mapping coordinate variables.', schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'no-empty-attrs': Rule(meta=RuleMeta(name='no-empty-attrs', version='1.0.0', description='Every dataset element should have metadata that describes it.', schema=None, ref=None, docs_url=None, type='suggestion'), op_class=), 'time-coords': Rule(meta=RuleMeta(name='time-coords', version='1.0.0', description=\"Time coordinates (standard_name='time') should have unambiguous time encoding.\", schema=None, ref=None, docs_url=None, type='problem'), op_class=), 'var-units-attr': Rule(meta=RuleMeta(name='var-units-attr', version='1.0.0', description=\"Every variable should have a valid 'units' attribute.\", schema=None, ref=None, docs_url=None, type='suggestion'), op_class=)}, processors={}, configs={'recommended': Config(name='recommended', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), 'all': Config(name='all', files=None, ignores=None, linter_options=None, opener_options=None, processor=None, plugins=None, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=2, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=2, args=(), kwargs={})}, settings=None)})}, rules={'coords-for-dims': RuleConfig(severity=2, args=(), kwargs={}), 'dataset-title-attr': RuleConfig(severity=1, args=(), kwargs={}), 'grid-mappings': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coords': RuleConfig(severity=2, args=(), kwargs={}), 'var-units-attr': RuleConfig(severity=1, args=(), kwargs={})}, settings=None), file_path='', messages=[Message(message=\"Missing 'title' attribute in dataset.\", node_path='dataset', rule_id='dataset-title-attr', severity=1, fatal=None, fix=None, suggestions=None), Message(message='Missing metadata, attributes are empty.', node_path='dataset.attrs', rule_id='no-empty-attrs', severity=1, fatal=None, fix=None, suggestions=[Suggestion(desc='Make sure to add appropriate metadata attributes to dataset elements.', data=None, fix=None)]), Message(message=\"Missing timezone in 'units' attribute: days since 2020-01-01 ß0:000:00\", node_path=\"dataset.coords['time']\", rule_id='time-coords', severity=2, fatal=None, fix=None, suggestions=[Suggestion(desc=\"Specify 'units' attribute using format ' since