Skip to content

Commit

Permalink
Merge pull request #2688 from sfinkens/fix-time-parameters-decoding
Browse files Browse the repository at this point in the history
Decode time parameters in CF Reader
  • Loading branch information
mraspaud authored Feb 20, 2024
2 parents e74729e + 37853ad commit b9c6709
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 120 deletions.
74 changes: 74 additions & 0 deletions satpy/cf/decoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 Satpy developers
#
# This file is part of satpy.
#
# satpy is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# satpy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# satpy. If not, see <http://www.gnu.org/licenses/>.
"""CF decoding."""
import copy
import json
from datetime import datetime


def decode_attrs(attrs):
"""Decode CF-encoded attributes to Python object.
Converts timestamps to datetime and strings starting with "{" to
dictionary.
Args:
attrs (dict): Attributes to be decoded
Returns (dict): Decoded attributes
"""
attrs = copy.deepcopy(attrs)
_decode_dict_type_attrs(attrs)
_decode_timestamps(attrs)
return attrs


def _decode_dict_type_attrs(attrs):
for key, val in attrs.items():
attrs[key] = _str2dict(val)


def _str2dict(val):
"""Convert string to dictionary."""
if isinstance(val, str) and val.startswith("{"):
val = json.loads(val, object_hook=_datetime_parser_json)
return val


def _decode_timestamps(attrs):
for key, value in attrs.items():
timestamp = _str2datetime(value)
if timestamp:
attrs[key] = timestamp


def _datetime_parser_json(json_dict):
"""Traverse JSON dictionary and parse timestamps."""
for key, value in json_dict.items():
timestamp = _str2datetime(value)
if timestamp:
json_dict[key] = timestamp
return json_dict


def _str2datetime(string):
"""Convert string to datetime object."""
try:
return datetime.fromisoformat(string)
except (TypeError, ValueError):
return None
13 changes: 2 additions & 11 deletions satpy/readers/satpy_cf_nc.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,12 @@
"""
import itertools
import json
import logging

import xarray as xr
from pyresample import AreaDefinition

import satpy.cf.decoding
from satpy.dataset.dataid import WavelengthRange
from satpy.readers.file_handlers import BaseFileHandler
from satpy.utils import get_legacy_chunk_size
Expand Down Expand Up @@ -311,9 +311,7 @@ def get_dataset(self, ds_id, ds_info):
if name != ds_id["name"]:
data = data.rename(ds_id["name"])
data.attrs.update(nc.attrs) # For now add global attributes to all datasets
if "orbital_parameters" in data.attrs:
data.attrs["orbital_parameters"] = _str2dict(data.attrs["orbital_parameters"])

data.attrs = satpy.cf.decoding.decode_attrs(data.attrs)
return data

def get_area_def(self, dataset_id):
Expand All @@ -327,10 +325,3 @@ def get_area_def(self, dataset_id):
# with the yaml_reader NotImplementedError is raised.
logger.debug("No AreaDefinition to load from nc file. Falling back to SwathDefinition.")
raise NotImplementedError


def _str2dict(val):
"""Convert string to dictionary."""
if isinstance(val, str):
val = json.loads(val)
return val
64 changes: 64 additions & 0 deletions satpy/tests/cf_tests/test_decoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 Satpy developers
#
# This file is part of satpy.
#
# satpy is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# satpy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# satpy. If not, see <http://www.gnu.org/licenses/>.
"""Tests for CF decoding."""
from datetime import datetime

import pytest

import satpy.cf.decoding


class TestDecodeAttrs:
"""Test decoding of CF-encoded attributes."""

@pytest.fixture()
def attrs(self):
"""Get CF-encoded attributes."""
return {
"my_integer": 0,
"my_float": 0.0,
"my_list": [1, 2, 3],
"my_timestamp1": "2000-01-01",
"my_timestamp2": "2000-01-01 12:15:33",
"my_timestamp3": "2000-01-01 12:15:33.123456",
"my_dict": '{"a": {"b": [1, 2, 3]}, "c": {"d": "2000-01-01 12:15:33.123456"}}'
}

@pytest.fixture()
def expected(self):
"""Get expected decoded results."""
return {
"my_integer": 0,
"my_float": 0.0,
"my_list": [1, 2, 3],
"my_timestamp1": datetime(2000, 1, 1),
"my_timestamp2": datetime(2000, 1, 1, 12, 15, 33),
"my_timestamp3": datetime(2000, 1, 1, 12, 15, 33, 123456),
"my_dict": {"a": {"b": [1, 2, 3]},
"c": {"d": datetime(2000, 1, 1, 12, 15, 33, 123456)}}
}

def test_decoding(self, attrs, expected):
"""Test decoding of CF-encoded attributes."""
res = satpy.cf.decoding.decode_attrs(attrs)
assert res == expected

def test_decoding_doesnt_modify_original(self, attrs):
"""Test that decoding doesn't modify the original attributes."""
satpy.cf.decoding.decode_attrs(attrs)
assert isinstance(attrs["my_dict"], str)
Loading

0 comments on commit b9c6709

Please sign in to comment.