diff --git a/CHANGES.md b/CHANGES.md
index 5e76412..2cf7b1a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -4,6 +4,8 @@
- Added more rules
- core rule "flags"
+ - core rule "lon-coordinate"
+ - core rule "lat-coordinate"
- core rule "time-coordinate" (#15)
- xcube rule "time-naming" (#15)
diff --git a/docs/rule-ref.md b/docs/rule-ref.md
index 2a8c46f..6db23a8 100644
--- a/docs/rule-ref.md
+++ b/docs/rule-ref.md
@@ -30,6 +30,20 @@ Grid mappings, if any, shall have valid grid mapping coordinate variables.
Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt:
+### :material-bug: `lat-coordinate`
+
+Latitude coordinate should have standard units and standard names.
+[More information.](https://cfconventions.org/cf-conventions/cf-conventions.html#latitude-coordinate)
+
+Contained in: `all`-:material-lightning-bolt:
+
+### :material-bug: `lon-coordinate`
+
+Longitude coordinate should have standard units and standard names.
+[More information.](https://cfconventions.org/cf-conventions/cf-conventions.html#longitude-coordinate)
+
+Contained in: `all`-:material-lightning-bolt:
+
### :material-lightbulb: `no-empty-attrs`
Every dataset element should have metadata that describes it.
diff --git a/notebooks/mkdataset.py b/notebooks/mkdataset.py
index bb64a9b..39b9a04 100644
--- a/notebooks/mkdataset.py
+++ b/notebooks/mkdataset.py
@@ -13,10 +13,18 @@ def make_dataset() -> xr.Dataset:
attrs=dict(title="SST-Climatology Subset"),
coords={
"x": xr.DataArray(
- np.linspace(-180, 180, nx), dims="x", attrs={"units": "degrees"}
+ np.linspace(-180, 180, nx), dims="x", attrs={
+ "standard_name": "longitude",
+ "long_name": "longitude",
+ "units": "degrees_east"
+ }
),
"y": xr.DataArray(
- np.linspace(-90, 90, ny), dims="y", attrs={"units": "degrees"}
+ np.linspace(-90, 90, ny), dims="y", attrs={
+ "standard_name": "latitude",
+ "long_name": "latitude",
+ "units": "degrees_north"
+ }
),
"time": xr.DataArray(
[365 * i for i in range(nt)],
@@ -55,6 +63,10 @@ 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.x.attrs["units"] = "degrees"
+ invalid_ds.x.attrs["axis"] = "x"
+ del invalid_ds.y.attrs["standard_name"]
+ invalid_ds.y.attrs["axis"] = "y"
invalid_ds.time.attrs["units"] = "days since 2020-01-01 ß0:000:00"
invalid_ds.attrs = {}
invalid_ds.sst.attrs["units"] = 1
diff --git a/notebooks/xrlint-linter.ipynb b/notebooks/xrlint-linter.ipynb
index b8474e7..0c1e987 100644
--- a/notebooks/xrlint-linter.ipynb
+++ b/notebooks/xrlint-linter.ipynb
@@ -437,38 +437,38 @@
" * 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.3002 0.935 ... 0.2138 0.04308\n",
- " sst_anomaly (time, y, x) float64 192B 0.1556 0.3582 ... 0.4877 0.04322\n",
+ " sst (time, y, x) float64 192B 0.357 0.4776 0.8956 ... 0.2892 0.1911\n",
+ " sst_anomaly (time, y, x) float64 192B 0.1693 0.4643 ... 0.6467 0.9646\n",
"Attributes:\n",
- " title: SST-Climatology Subset
PandasIndex
PandasIndex(Index([-180.0, 180.0], dtype='float64', name='x'))
PandasIndex
PandasIndex(Index([-90.0, 0.0, 90.0], dtype='float64', name='y'))
PandasIndex
PandasIndex(Index([0, 365, 730, 1095], dtype='int64', name='time'))
- title :
- SST-Climatology Subset
"
+ " title: SST-Climatology SubsetPandasIndex
PandasIndex(Index([-180.0, 180.0], dtype='float64', name='x'))
PandasIndex
PandasIndex(Index([-90.0, 0.0, 90.0], dtype='float64', name='y'))
PandasIndex
PandasIndex(Index([0, 365, 730, 1095], dtype='int64', name='time'))
- title :
- SST-Climatology Subset
"
],
"text/plain": [
" Size: 464B\n",
@@ -479,8 +479,8 @@
" * 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.3002 0.935 ... 0.2138 0.04308\n",
- " sst_anomaly (time, y, x) float64 192B 0.1556 0.3582 ... 0.4877 0.04322\n",
+ " sst (time, y, x) float64 192B 0.357 0.4776 0.8956 ... 0.2892 0.1911\n",
+ " sst_anomaly (time, y, x) float64 192B 0.1693 0.4643 ... 0.6467 0.9646\n",
"Attributes:\n",
" title: SST-Climatology Subset"
]
@@ -516,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=), '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)"
+ "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=), 'lat-coordinate': Rule(meta=RuleMeta(name='lat-coordinate', version='1.0.0', description='Latitude coordinate should have standard units and standard names.', schema=None, ref=None, docs_url='https://cfconventions.org/cf-conventions/cf-conventions.html#latitude-coordinate', type='problem'), op_class=), 'lon-coordinate': Rule(meta=RuleMeta(name='lon-coordinate', version='1.0.0', description='Longitude coordinate should have standard units and standard names.', schema=None, ref=None, docs_url='https://cfconventions.org/cf-conventions/cf-conventions.html#longitude-coordinate', 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-coordinate': Rule(meta=RuleMeta(name='time-coordinate', version='1.0.0', description=\"Time coordinate (standard_name='time') should have unambiguous time units encoding.\", schema=None, ref=None, docs_url='https://cfconventions.org/cf-conventions/cf-conventions.html#time-coordinate', 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={}), 'lat-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'lon-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coordinate': 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={}), 'lat-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'lon-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=2, args=(), kwargs={}), 'time-coordinate': 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={}), 'lat-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'lon-coordinate': RuleConfig(severity=2, args=(), kwargs={}), 'no-empty-attrs': RuleConfig(severity=1, args=(), kwargs={}), 'time-coordinate': 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,
@@ -914,38 +914,38 @@
" * 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.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.889sst
(time, y, x)
float64
0.8253 0.09788 ... 0.9227 0.03201
- units :
- 1
- grid_mapping :
- spatial_ref
array([[[0.82533638, 0.09788167],\n",
- " [0.36302602, 0.06317208],\n",
- " [0.51267858, 0.0508812 ]],\n",
- "\n",
- " [[0.08989961, 0.15860013],\n",
- " [0.33914745, 0.27724928],\n",
- " [0.61942268, 0.35260964]],\n",
- "\n",
- " [[0.07532258, 0.74068453],\n",
- " [0.44470924, 0.64692307],\n",
- " [0.69413451, 0.29770663]],\n",
- "\n",
- " [[0.2938846 , 0.57585369],\n",
- " [0.54826241, 0.06475186],\n",
- " [0.92269265, 0.03201194]]])
sst_anomaly
(time, y, x)
float64
0.1763 0.1965 ... 0.8407 0.6847
- units :
- kelvin
- grid_mapping :
- spatial_ref
array([[[0.17630954, 0.1964818 ],\n",
- " [0.01663852, 0.92539483],\n",
- " [0.71353744, 0.79983157]],\n",
- "\n",
- " [[0.42714948, 0.73404844],\n",
- " [0.3826761 , 0.58874282],\n",
- " [0.23307288, 0.55076838]],\n",
- "\n",
- " [[0.9545314 , 0.21068175],\n",
- " [0.23213064, 0.4574122 ],\n",
- " [0.55712704, 0.21308774]],\n",
- "\n",
- " [[0.81147577, 0.94752373],\n",
- " [0.48260933, 0.1147288 ],\n",
- " [0.84069412, 0.68471572]]])
sst_avg
(x, y)
float64
0.2573 0.7063 ... 0.7286 0.889
array([[0.25732809, 0.70629655, 0.87638115],\n",
- " [0.21889192, 0.72861022, 0.88901369]])
PandasIndex
PandasIndex(Index([-180.0, 180.0], dtype='float64', name='x'))
PandasIndex
PandasIndex(Index([-90.0, 0.0, 90.0], dtype='float64', name='y'))
PandasIndex
PandasIndex(Index([0, 365, 730, 1095], dtype='int64', name='time'))
"
+ " sst (time, y, x) float64 192B 0.9673 0.8684 ... 0.6397 0.2261\n",
+ " sst_anomaly (time, y, x) float64 192B 0.5013 0.1469 0.03853 ... 0.51 0.9694\n",
+ " sst_avg (x, y) float64 48B 0.5494 0.02223 0.5723 0.4118 0.1869 0.872sst
(time, y, x)
float64
0.9673 0.8684 ... 0.6397 0.2261
- units :
- 1
- grid_mapping :
- spatial_ref
array([[[0.96731725, 0.8684109 ],\n",
+ " [0.39619977, 0.59997665],\n",
+ " [0.63287615, 0.89969016]],\n",
+ "\n",
+ " [[0.9319926 , 0.58000176],\n",
+ " [0.82350634, 0.34917117],\n",
+ " [0.86295035, 0.28466155]],\n",
+ "\n",
+ " [[0.54803602, 0.51662934],\n",
+ " [0.52600463, 0.6926919 ],\n",
+ " [0.85294568, 0.30917634]],\n",
+ "\n",
+ " [[0.32025665, 0.22962375],\n",
+ " [0.10266749, 0.23635291],\n",
+ " [0.63968365, 0.2260538 ]]])
sst_anomaly
(time, y, x)
float64
0.5013 0.1469 ... 0.51 0.9694
- units :
- kelvin
- grid_mapping :
- spatial_ref
array([[[0.5012984 , 0.14688672],\n",
+ " [0.03852958, 0.87815287],\n",
+ " [0.15632951, 0.76000962]],\n",
+ "\n",
+ " [[0.44990288, 0.45157612],\n",
+ " [0.57644988, 0.1373151 ],\n",
+ " [0.83785531, 0.24725167]],\n",
+ "\n",
+ " [[0.20564946, 0.67642054],\n",
+ " [0.035706 , 0.96148924],\n",
+ " [0.39731164, 0.13817667]],\n",
+ "\n",
+ " [[0.58632619, 0.71732863],\n",
+ " [0.13652191, 0.57510901],\n",
+ " [0.50999833, 0.96943729]]])
sst_avg
(x, y)
float64
0.5494 0.02223 ... 0.1869 0.872
array([[0.5494279 , 0.02223383, 0.57233653],\n",
+ " [0.41180829, 0.18686494, 0.87197388]])
PandasIndex
PandasIndex(Index([-180.0, 180.0], dtype='float64', name='x'))
PandasIndex
PandasIndex(Index([-90.0, 0.0, 90.0], dtype='float64', name='y'))
PandasIndex
PandasIndex(Index([0, 365, 730, 1095], dtype='int64', name='time'))
"
],
"text/plain": [
" Size: 512B\n",
@@ -956,9 +956,9 @@
" * 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.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"
+ " sst (time, y, x) float64 192B 0.9673 0.8684 ... 0.6397 0.2261\n",
+ " sst_anomaly (time, y, x) float64 192B 0.5013 0.1469 0.03853 ... 0.51 0.9694\n",
+ " sst_avg (x, y) float64 48B 0.5494 0.02223 0.5723 0.4118 0.1869 0.872"
]
},
"execution_count": 6,
@@ -985,14 +985,18 @@
"\n",
"\n",
"| dataset | warn | Missing 'title' attribute in dataset. | dataset-title-attr |
\n",
+ "| dataset.coords['y'] | error | Attribute 'standard_name' should be 'latitude', was None. | lat-coordinate |
\n",
+ "| dataset.coords['y'] | error | Attribute 'axis' should be 'Y', was 'y'. | lat-coordinate |
\n",
+ "| dataset.coords['x'] | error | Attribute 'units' should be 'degrees_east', was 'degrees'. | lon-coordinate |
\n",
+ "| dataset.coords['x'] | error | Attribute 'axis' should be 'X', was 'x'. | lon-coordinate |
\n",
"| dataset.attrs | warn | Missing metadata, attributes are empty. | no-empty-attrs |
\n",
- "| dataset.coords['time'] | error | Missing timezone in 'units' attribute: days since 2020-01-01 ß0:000:00 | time-coords |
\n",
+ "| dataset.coords['time'] | error | Missing timezone in 'units' attribute: days since 2020-01-01 ß0:000:00 | time-coordinate |
\n",
"| dataset.data_vars['sst'] | warn | Invalid 'units' attribute in variable 'sst'. | var-units-attr |
\n",
"\n",
- "
4 problems (one error and 3 warnings)
\n"
+ "8 problems (5 errors 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=), '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