diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a83a80c..e64c1c4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,11 +2,19 @@ Changelog for eccodes-python ============================ +2.38.0 (2024-MM-DD) +-------------------- + +- ECC-1790: Add codes_get_offset +- ECC-1899: API function to allow setting debug level +- Function to query library features + 2.37.0 (2024-09-09) ------------------- - bundle ecCodes binary library with the PyPi distribution, for Linux and MacOS + 1.7.1 (2024-06-19) -------------------- diff --git a/README.rst b/README.rst index 94128b0..faf4444 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Installation follow the version numbering of the ecCodes binary library. See below for details.** -The package is installed from PyPI with:: +The package can be installed from PyPI with:: $ pip install eccodes diff --git a/ci/requirements-dev.txt b/ci/requirements-dev.txt new file mode 100644 index 0000000..8f5e14d --- /dev/null +++ b/ci/requirements-dev.txt @@ -0,0 +1,15 @@ +check-manifest +detox +IPython +matplotlib +notebook +pip-tools +pyroma +pytest-mypy +setuptools +tox +tox-pyenv +wheel +zest.releaser +black + diff --git a/ci/requirements-docs.in b/ci/requirements-docs.in new file mode 100644 index 0000000..9817086 --- /dev/null +++ b/ci/requirements-docs.in @@ -0,0 +1,3 @@ +Sphinx +pytest-runner + diff --git a/ci/requirements-docs.txt b/ci/requirements-docs.txt new file mode 100644 index 0000000..d148bc2 --- /dev/null +++ b/ci/requirements-docs.txt @@ -0,0 +1,48 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file ci/requirements-docs.txt setup.py ci/requirements-docs.in +# +alabaster==0.7.12 + # via sphinx +babel==2.9.1 + # via sphinx +certifi==2024.7.4 + # via requests +charset-normalizer==3.3.2 + # via requests +docutils==0.14 + # via sphinx +idna==3.7 + # via requests +imagesize==1.1.0 + # via sphinx +jinja2==3.1.4 + # via sphinx +markupsafe==2.1.5 + # via jinja2 +packaging==19.0 + # via sphinx +pygments==2.15.0 + # via sphinx +pyparsing==2.3.1 + # via packaging +pytest-runner==4.4 + # via -r requirements-docs.in +pytz==2018.9 + # via babel +requests==2.32.2 + # via sphinx +six==1.12.0 + # via + # packaging + # sphinx +snowballstemmer==1.2.1 + # via sphinx +sphinx==1.8.5 + # via -r requirements-docs.in +sphinxcontrib-websupport==1.1.0 + # via sphinx +urllib3==1.26.19 + # via requests diff --git a/ci/requirements-tests.in b/ci/requirements-tests.in new file mode 100644 index 0000000..7d5c983 --- /dev/null +++ b/ci/requirements-tests.in @@ -0,0 +1,6 @@ +pytest +pytest-cov +pytest-flakes +pytest-mccabe +pytest-pep8 +pytest-runner diff --git a/ci/requirements-tests.txt b/ci/requirements-tests.txt new file mode 100644 index 0000000..b7e1f6b --- /dev/null +++ b/ci/requirements-tests.txt @@ -0,0 +1,52 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file ci/requirements-tests.txt setup.py ci/requirements-tests.in +# +apipkg==1.5 + # via execnet +atomicwrites==1.3.0 + # via pytest +attrs==19.1.0 + # via pytest +coverage==4.5.3 + # via pytest-cov +execnet==1.5.0 + # via pytest-cache +mccabe==0.6.1 + # via pytest-mccabe +more-itertools==5.0.0 + # via pytest +pep8==1.7.1 + # via pytest-pep8 +pluggy==0.9.0 + # via pytest +pyflakes==2.1.1 + # via pytest-flakes +pytest==4.3.1 + # via + # -r requirements-tests.in + # pytest-cache + # pytest-cov + # pytest-flakes + # pytest-mccabe + # pytest-pep8 +pytest-cache==1.0 + # via + # pytest-mccabe + # pytest-pep8 +pytest-cov==2.6.1 + # via -r requirements-tests.in +pytest-flakes==4.0.0 + # via -r requirements-tests.in +pytest-mccabe==0.1 + # via -r requirements-tests.in +pytest-pep8==1.0.6 + # via -r requirements-tests.in +pytest-runner==4.4 + # via -r requirements-tests.in +six==1.12.0 + # via + # more-itertools + # pytest diff --git a/eccodes/eccodes.py b/eccodes/eccodes.py index ef8640e..0bb0a49 100644 --- a/eccodes/eccodes.py +++ b/eccodes/eccodes.py @@ -10,6 +10,9 @@ # # from gribapi import ( + CODES_FEATURES_ALL, + CODES_FEATURES_DISABLED, + CODES_FEATURES_ENABLED, CODES_PRODUCT_ANY, CODES_PRODUCT_BUFR, CODES_PRODUCT_GRIB, @@ -44,6 +47,7 @@ codes_dump, codes_extract_offsets, codes_extract_offsets_sizes, + codes_get_features, codes_get_gaussian_latitudes, codes_get_library_path, codes_get_version_info, @@ -73,6 +77,7 @@ from gribapi import grib_get_message_offset as codes_get_message_offset from gribapi import grib_get_message_size as codes_get_message_size from gribapi import grib_get_native_type as codes_get_native_type +from gribapi import grib_get_offset as codes_get_offset from gribapi import grib_get_size as codes_get_size from gribapi import grib_get_string as codes_get_string from gribapi import grib_get_string_array as codes_get_string_array @@ -123,6 +128,7 @@ from gribapi import grib_release as codes_release from gribapi import grib_set as codes_set from gribapi import grib_set_array as codes_set_array +from gribapi import grib_set_debug as codes_set_debug from gribapi import grib_set_definitions_path as codes_set_definitions_path from gribapi import grib_set_double as codes_set_double from gribapi import grib_set_double_array as codes_set_double_array @@ -247,6 +253,9 @@ "codes_definition_path", "codes_extract_offsets", "codes_extract_offsets_sizes", + "CODES_FEATURES_ALL", + "CODES_FEATURES_ENABLED", + "CODES_FEATURES_DISABLED", "codes_get_api_version", "codes_get_array", "codes_get_double_array", @@ -263,6 +272,7 @@ "codes_get_message_size", "codes_get_message", "codes_get_native_type", + "codes_get_offset", "codes_get_size", "codes_get_string_array", "codes_get_string_length", @@ -270,6 +280,7 @@ "codes_get_values", "codes_get_version_info", "codes_get", + "codes_get_features", "codes_grib_find_nearest_multiple", "codes_grib_find_nearest", "codes_grib_get_data", @@ -333,6 +344,7 @@ "codes_samples_path", "codes_dump", "codes_set_array", + "codes_set_debug", "codes_set_definitions_path", "codes_set_double_array", "codes_set_double", diff --git a/gribapi/bindings.py b/gribapi/bindings.py index 6ee45d3..6d569e5 100644 --- a/gribapi/bindings.py +++ b/gribapi/bindings.py @@ -23,7 +23,7 @@ import cffi -__version__ = "2.37.0" +__version__ = "2.38.0" LOG = logging.getLogger(__name__) diff --git a/gribapi/errors.py b/gribapi/errors.py index 520d223..891a2ac 100644 --- a/gribapi/errors.py +++ b/gribapi/errors.py @@ -26,7 +26,7 @@ class GribInternalError(Exception): def __init__(self, value): # Call the base class constructor with the parameters it needs Exception.__init__(self, value) - if type(value) is int: + if isinstance(value, int): self.msg = ffi.string(lib.grib_get_error_message(value)).decode(ENC) else: self.msg = value diff --git a/gribapi/grib_api.h b/gribapi/grib_api.h index e0df7b9..96f4daf 100644 --- a/gribapi/grib_api.h +++ b/gribapi/grib_api.h @@ -100,6 +100,7 @@ int grib_nearest_find_multiple(const grib_handle* h, int is_lsm, double* outlats, double* outlons, double* values, double* distances, int* indexes); +int grib_get_offset(const grib_handle* h, const char* key, size_t* offset); int grib_get_size(const grib_handle* h, const char* key,size_t *size); int grib_get_length(const grib_handle* h, const char* key,size_t *length); @@ -107,7 +108,7 @@ int grib_get_long(const grib_handle* h, const char* key, long* value); int grib_get_double(const grib_handle* h, const char* key, double* value); int grib_get_double_element(const grib_handle* h, const char* key, int i, double* value); int grib_get_double_elements(const grib_handle* h, const char* key, const int* index_array, long size, double* value); -int grib_get_string(const grib_handle* h, const char* key, char* mesg, size_t *length); +int grib_get_string(const grib_handle* h, const char* key, char* value, size_t *length); int grib_get_string_array(const grib_handle* h, const char* key, char** vals, size_t *length); int grib_get_double_array(const grib_handle* h, const char* key, double* vals, size_t *length); int grib_get_float_array(const grib_handle* h, const char* key, float* vals, size_t *length); @@ -131,6 +132,7 @@ void grib_gts_header_off(grib_context* c); void grib_gribex_mode_on(grib_context* c); void grib_gribex_mode_off(grib_context* c); void grib_context_set_definitions_path(grib_context* c, const char* path); +void grib_context_set_debug(grib_context* c, int mode); void grib_context_set_samples_path(grib_context* c, const char* path); void grib_multi_support_on(grib_context* c); void grib_multi_support_off(grib_context* c); @@ -174,6 +176,9 @@ int parse_keyval_string(const char *grib_tool, char *arg, int values_required, i int grib_get_data(const grib_handle *h, double *lats, double *lons, double *values); int grib_get_gaussian_latitudes(long trunc, double* lats); +int codes_is_feature_enabled(const char* feature); +int codes_get_features(char* result, size_t* length, int select); + /* EXPERIMENTAL */ typedef struct codes_bufr_header { unsigned long message_offset; diff --git a/gribapi/gribapi.py b/gribapi/gribapi.py index 89b0788..bef3137 100644 --- a/gribapi/gribapi.py +++ b/gribapi/gribapi.py @@ -70,6 +70,12 @@ GRIB_NEAREST_SAME_DATA = 1 << 1 GRIB_NEAREST_SAME_POINT = 1 << 2 +# Constants for feature selection +CODES_FEATURES_ALL = 0 +CODES_FEATURES_ENABLED = 1 +CODES_FEATURES_DISABLED = 2 + + # ECC-1029: Disable function-arguments type-checking unless # environment variable is defined and equal to 1 enable_type_checks = os.environ.get("ECCODES_PYTHON_ENABLE_TYPE_CHECKS") == "1" @@ -578,6 +584,23 @@ def grib_multi_append(ingribid, startsection, multigribid): GRIB_CHECK(lib.grib_multi_handle_append(h, startsection, mh)) +@require(msgid=int, key=str) +def grib_get_offset(msgid, key): + """ + @brief Get the byte offset of a key. If several keys of the same name + are present, the offset of the last one is returned + + @param msgid id of the message loaded in memory + @param key name of the key + @exception CodesInternalError + """ + h = get_handle(msgid) + offset_p = ffi.new("size_t*") + err = lib.grib_get_offset(h, key.encode(ENC), offset_p) + GRIB_CHECK(err) + return offset_p[0] + + @require(msgid=int, key=str) def grib_get_size(msgid, key): """ @@ -2405,6 +2428,16 @@ def codes_samples_path(): return ffi.string(spath).decode(ENC) +def grib_set_debug(dmode): + """ + @brief Set the debug mode + + @param dmode -1, 0 or 1 + """ + context = lib.grib_context_get_default() + lib.grib_context_set_debug(context, dmode) + + @require(defs_path=str) def grib_set_definitions_path(defs_path): """ @@ -2617,6 +2650,23 @@ def codes_extract_offsets_sizes(filepath, product_kind, is_strict=True): i += 1 +@require(select=int) +def codes_get_features(select=CODES_FEATURES_ALL): + """ + @brief Get the list of library features. + + @param select One of CODES_FEATURES_ALL, CODES_FEATURES_ENABLED or CODES_FEATURES_DISABLED + @return space-separated string of feature names + @exception CodesInternalError + """ + ssize = 1024 + result = ffi.new("char[]", ssize) + size_p = ffi.new("size_t *", ssize) + err = lib.codes_get_features(result, size_p, select) + GRIB_CHECK(err) + return ffi.string(result).decode(ENC) + + # ------------------------------- # EXPERIMENTAL FEATURES # ------------------------------- diff --git a/tests/test_eccodes.py b/tests/test_eccodes.py index 7416fe7..44cc38e 100644 --- a/tests/test_eccodes.py +++ b/tests/test_eccodes.py @@ -48,6 +48,13 @@ def test_codes_samples_path(): assert sp is not None +def test_codes_set_debug(): + if eccodes.codes_get_api_version(int) < 23700: + pytest.skip("ecCodes version too old") + eccodes.codes_set_debug(-1) + eccodes.codes_set_debug(0) + + def test_codes_set_definitions_path(): eccodes.codes_set_definitions_path(eccodes.codes_definition_path()) @@ -58,11 +65,11 @@ def test_codes_set_samples_path(): def test_api_version(): vs = eccodes.codes_get_api_version() - assert type(vs) is str + assert isinstance(vs, str) assert len(vs) > 0 assert vs == eccodes.codes_get_api_version(str) vi = eccodes.codes_get_api_version(int) - assert type(vi) is int + assert isinstance(vi, int) assert vi > 20000 print(vi) @@ -73,6 +80,18 @@ def test_version_info(): assert len(vinfo) == 2 +def test_codes_get_features(): + if eccodes.codes_get_api_version(int) < 23700: + pytest.skip("ecCodes version too old") + + features = eccodes.codes_get_features(eccodes.CODES_FEATURES_ALL) + print(f"\n\tAll features = {features}") + features = eccodes.codes_get_features(eccodes.CODES_FEATURES_DISABLED) + print(f"\tDisabled features = {features}") + features = eccodes.codes_get_features(eccodes.CODES_FEATURES_ENABLED) + print(f"\tEnabled features = {features}") + + def test_codes_is_defined(): gid = eccodes.codes_grib_new_from_samples("sh_sfc_grib1") assert eccodes.codes_is_defined(gid, "JS") @@ -358,6 +377,14 @@ def test_grib_get_message_offset(): assert eccodes.codes_get_message_offset(gid) == 0 +def test_grib_get_key_offset(): + gid = eccodes.codes_grib_new_from_samples("GRIB2") + assert eccodes.codes_get_offset(gid, "identifier") == 0 + assert eccodes.codes_get_offset(gid, "discipline") == 6 + assert eccodes.codes_get_offset(gid, "offsetSection1") == 16 + assert eccodes.codes_get_offset(gid, "7777") == 175 + + def test_grib_clone(): gid = eccodes.codes_grib_new_from_samples("GRIB2") clone = eccodes.codes_clone(gid) @@ -725,7 +752,7 @@ def test_grib_uuid_get_set(): eccodes.codes_set(gid, "gridType", "unstructured_grid") key = "uuidOfHGrid" ntype = eccodes.codes_get_native_type(gid, key) - assert ntype == bytes + assert ntype is bytes uuid = eccodes.codes_get_string(gid, key) assert uuid == "00000000000000000000000000000000"