Repository: ecmwf/earthkit-data
Pull request #136 (2023-07-02 19:32:48)

Improve BUFR documentation and tests
Add more BUFR documentation and tests


Patch:
diff --git a/docs/api.rst b/docs/api.rst
index 3ff050f..46d7a08 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -11,11 +11,11 @@ GRIB
 - :py:class:`FieldList <data.readers.grib.index.FieldList>`
 - :py:class:`GribField <data.readers.grib.codes.GribField>`
 
-.. BUFR
-.. -----
-
-.. - :py:class:`BufrReader <data.readers.bufr.BUFRReader>`
+BUFR
+-----
 
+- :py:class:`BUFRList <data.readers.bufr.bufr.BUFRList>`
+- :py:class:`BUFRMessage <data.readers.bufr.bufr.BUFRMessage>`
 
 Other
 --------
diff --git a/docs/conf.py b/docs/conf.py
index ae19a73..c587a32 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,6 +10,7 @@ import datetime
 import os
 import sys
 
+sys.path.insert(0, os.path.abspath("./"))
 sys.path.insert(0, os.path.abspath("../"))
 
 # -- Project information -----------------------------------------------------
@@ -108,44 +109,7 @@ xref_links = {
 }
 
 
-# define skip rules for autoapi
-def _skip_for_api(app, what, name, obj, skip, options):
-    # if what == "package":
-    #     print(f"{name}[{what}]")
-
-    if what == "module" and name not in [
-        "data.readers",
-        "data.readers.grib.codes",
-        "data.readers.grib.index",
-        "data.utils",
-        "data.utils.bbox",
-    ]:
-        skip = True
-    elif what == "package" and name not in [
-        "data",
-        "data.readers",
-        "data.readers.grib",
-        "data.readers.grib.index",
-        "data.utils",
-        "data.utils.bbox",
-    ]:
-        skip = True
-    elif what == "class" and name not in [
-        "data.readers.bufr.BUFRReader",
-        "data.readers.grib.codes.GribField",
-        "data.readers.grib.index.FieldList",
-        "data.utils.bbox.BoundingBox",
-    ]:
-        skip = True
-    elif what == "method" and "abstractmethod" in getattr(obj, "properties", []):
-        skip = True
-    elif what in ("function", "attribute", "data"):
-        skip = True
-
-    if not skip:
-        print(f"{what} {name}")
-    return skip
-
-
 def setup(app):
-    app.connect("autoapi-skip-member", _skip_for_api)
+    from skip_api_rules import _skip_api_items
+
+    app.connect("autoapi-skip-member", _skip_api_items)
diff --git a/docs/guide/data.rst b/docs/guide/data.rst
index f1434ee..c1fa7cb 100644
--- a/docs/guide/data.rst
+++ b/docs/guide/data.rst
@@ -55,7 +55,7 @@ Data objects can be concatenated with the "+" operator:
 Iteration
 ~~~~~~~~~
 
-When an earthkit-data data `source` or dataset provides a list of fields, it can be iterated over to access each field (in a given order see :ref:`below <order_by>`).
+When an earthkit-data data `source` or dataset provides a list of fields or messages, we can iterated through it to access each element (in a given order see :ref:`below <order_by>`).
 
 In the the following example we read a GRIB file from disk. In the iteration each element is a field (representing a GRIB message):
 
@@ -82,7 +82,7 @@ In the the following example we read a GRIB file from disk. In the iteration eac
 Selection with ``[...]``
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-When an earthkit-data data `source` or dataset provides a list of fields, a subset of the list can be created using the standard python list interface relying on brackets and slices. A subsetting also works by providing a list or ndarray of indices.
+When an earthkit-data data `source` or dataset provides a list of fields or messages, a subset of it can be created using the standard python list interface relying on brackets and slices. Slicing also works by providing a list or ndarray of indices.
 
 .. code-block:: python
 
@@ -132,7 +132,7 @@ When an earthkit-data data `source` or dataset provides a list of fields, a subs
 Selection with ``.sel()``
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
-When an earthkit-data data `source` or dataset provides a list of fields, the method ``.sel()`` allows filtering this list and we can **select a subset** of the list of fields. ``.sel()`` returns a view to original data, so no data is copied. The selection offers the same functionality as the original data object, so methods like ``.to_numpy()``, ``.to_xarray()``, etc. are all available.
+When an earthkit-data data `source` or dataset provides a list of fields or messages, the method ``.sel()`` allows filtering this list and we can **select a subset** of the list. ``.sel()`` returns a view to original data, so no data is copied. The selection offers the same functionality as the original data object, so methods like ``.to_numpy()``, ``.to_xarray()``, etc. are all available.
 
 For more details see: :meth:`FieldList.sel() <data.readers.grib.index.FieldList.sel>`
 
@@ -179,7 +179,7 @@ The following example demonstrates the usage of ``.sel()``. The input data conta
 Selection with ``.isel()``
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-When an earthkit-data data `source` or dataset provides a list of fields, the method ``.isel()`` allows filtering this list and we can **select a subset** of the list of fields. ``.isel()`` returns a view to the original data, so no data is copied. The selection offers the same functionality as the original data object, so methods like ``.to_numpy()``, ``.to_xarray()`` , etc. are all available.
+When an earthkit-data data `source` or dataset provides a list of fields, the method ``.isel()`` allows filtering this list and we can **select a subset** of the list. ``.isel()`` returns a view to the original data, so no data is copied. The selection offers the same functionality as the original data object, so methods like ``.to_numpy()``, ``.to_xarray()`` , etc. are all available.
 
 ``.isel()`` works similarly to :ref:`sel <sel>` but conditions are specified by indices of metadata keys. A metadata index stores the unique, **sorted** values of the corresponding metadata key from all the fields in the input data.
 
@@ -229,7 +229,7 @@ The following example demonstrates the usage of ``.isel()``. The input data cont
 Ordering with ``.order_by()``
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-When an earthkit-data data `source` or dataset provides a list of fields, the method ``.order_by()`` allows sorting this list.
+When an earthkit-data data `source` or dataset provides a list of fields or messages, the method ``.order_by()`` allows sorting this list.
 
 ``.order_by()`` returns a "view" so no new data is generated on disk or in memory. The resulting object offers the same functionality as the original data object, so methods like ``.to_numpy()``, ``.to_xarray()``, etc. are all available.
 
@@ -328,7 +328,7 @@ Accessing metadata
 
 We can extract metadata from data objects using the ``.metadata()`` method.
 
-When an earthkit-data :ref:`source <data-sources>` provides a list of fields, this method can be called both on the whole object and on the individual fields, too.
+When an earthkit-data :ref:`source <data-sources>` provides a list of fields or messages, this method can be called both on the whole object and on the individual fields, too.
 
 For more details see: :meth:`FieldList.metadata() <data.readers.grib.index.FieldList.metadata>` and
 :meth:`GribField._metadata() <data.readers.grib.codes.GribField.metadata>`
diff --git a/docs/guide/data_format/bufr.rst b/docs/guide/data_format/bufr.rst
index 70c83e5..a01db1c 100644
--- a/docs/guide/data_format/bufr.rst
+++ b/docs/guide/data_format/bufr.rst
@@ -6,11 +6,47 @@ BUFR
 
 BUFR (Binary Universal Form for Representation of meteorological data) is a binary data format maintained by WMO. The earthkit-data interface supports both BUFR `edition 3 <https://community.wmo.int/en/activity-areas/wmo-codes/manual-codes/bufr-edition-3-and-crex-edition-1>`_ and `edition 4 <https://library.wmo.int/index.php?lvl=notice_display&id=10684>`_.
 
-to_pandas()
+BUFRList
++++++++++++
+
+We can read/retrieve BUFR data with :func:`from_source <data-sources>`. The resulting object will be a :obj:`BUFRList <data.readers.bufr.bufr.BUFRList>` representing a list of :obj:`BUFRMessage <data.readers.bufr.bufr.BUFRMessage>`.
+
+The structure of a BUFR message is typically hierarchical and can be rather complex, so the recommended way to deal with BUFR data is to extract the required data with :meth:`to_pandas() <data.readers.bufr.bufr.BUFRList.to_pandas>`
+into a pandas DataFrame, which is much easier to work with.
+
+The following table gives an overview of the :obj:`BUFRList API<data.readers.bufr.bufr.BUFRList>`:
+
+.. list-table:: Highlights of the BUFRList API
+   :header-rows: 1
+
+   * - Methods/Operators
+     - API
+   * - :ref:`conversion`
+     - :meth:`to_pandas() <data.readers.bufr.bufr.BUFRList.to_pandas>`
+   * - :ref:`concat`
+     -
+   * - :ref:`iter`
+     -
+   * - :ref:`slice`
+     -
+   * - :ref:`sel`
+     - :meth:`sel() <data.readers.bufr.bufr.BUFRList.sel>`
+   * - :ref:`order_by`
+     - :meth:`order_by() <data.readers.bufr.bufr.BUFRList.order_by>`
+   * - :ref:`metadata`
+     - :meth:`metadata() <data.readers.bufr.bufr.BUFRList.metadata>`
+   * - :ref:`inspection`
+     - :meth:`ls() <data.readers.bufr.bufr.BUFRList.ls>`, :meth:`head() <data.readers.bufr.bufr.BUFRList.head>`, :meth:`tail() <data.readers.bufr.bufr.BUFRList.tail>`
+
+BUFRMessage
 ++++++++++++++
 
-BUFR is a message based format and can contain both forecasts and observations. The structure of a BUFR message is typically hierarchical and can be rather complex, so the recommended way is to extract the required data with ``to_pandas()`` into a pandas DataFrame, which is much easier to work with. The BUFR data extraction in ``to_pandas()`` is implemented by :xref:`pdbufr`.
+A :obj:`BUFRMessage <data.readers.bufr.bufr.BUFRMessage>` represent a single BUFR message. It primarily offers methods to:
+
+ - extract message data/metadata with :meth:`metadata() <data.readers.bufr.bufr.BUFRMessage.metadata>`
+ - show the message structure with :meth:`dump() <data.readers.bufr.bufr.BUFRMessage.dump>`
 
 Examples:
 
-    - :ref:`/examples/bufr.ipynb`
+    - :ref:`/examples/bufr_temp.ipynb`
+    - :ref:`/examples/bufr_synop.ipynb`
diff --git a/docs/guide/data_format/grib.rst b/docs/guide/data_format/grib.rst
index 8d673f3..fdf8bf1 100644
--- a/docs/guide/data_format/grib.rst
+++ b/docs/guide/data_format/grib.rst
@@ -34,7 +34,7 @@ The following table gives an overview of the :obj:`FieldList API<data.readers.gr
    * - Methods/Operators
      - API
    * - :ref:`conversion`
-     - :meth:`FieldList.to_xarray() <data.readers.grib.index.FieldList.to_xarray>`
+     - :meth:`to_xarray() <data.readers.grib.index.FieldList.to_xarray>`
    * - :ref:`concat`
      -
    * - :ref:`iter`
@@ -42,23 +42,23 @@ The following table gives an overview of the :obj:`FieldList API<data.readers.gr
    * - :ref:`slice`
      -
    * - :ref:`sel`
-     - :meth:`FieldList.sel() <data.readers.grib.index.FieldList.sel>`
+     - :meth:`sel() <data.readers.grib.index.FieldList.sel>`
    * - :ref:`order_by`
-     - :meth:`FieldList.order_by() <data.readers.grib.index.FieldList.order_by>`
+     - :meth:`order_by() <data.readers.grib.index.FieldList.order_by>`
    * - :ref:`data_values`
-     - :meth:`FieldList.to_numpy() <data.readers.grib.index.FieldList.to_numpy>`
+     - :meth:`to_numpy() <data.readers.grib.index.FieldList.to_numpy>`
    * - :ref:`metadata`
-     - :meth:`FieldList.metadata() <data.readers.grib.index.FieldList.metadata>`
+     - :meth:`metadata() <data.readers.grib.index.FieldList.metadata>`
    * - :ref:`inspection`
-     - :meth:`FieldList.ls() <data.readers.grib.index.FieldList.ls>`, :meth:`FieldList.head() <data.readers.grib.index.FieldList.head>`
+     - :meth:`ls() <data.readers.grib.index.FieldList.ls>`, :meth:`head() <data.readers.grib.index.FieldList.head>` and :meth:`tail() <data.readers.grib.index.FieldList.tail>`
 
 Fields
 +++++++
 
 A :obj:`GribField <data.readers.grib.codes.GribField>` represent a single GRIB field. It primarily offers methods to:
 
- - :ref:`extract field values <data_values>`, such as :meth:`GribField.to_numpy() <data.readers.grib.codes.GribField.to_numpy>`
- - :ref:`extract field metadata <metadata>`, such as :meth:`GribField.metadata() <data.readers.grib.codes.GribField._metadata>`
+ - :ref:`extract field values <data_values>`, such as :meth:`to_numpy() <data.readers.grib.codes.GribField.to_numpy>`
+ - :ref:`extract field metadata <metadata>`, such as :meth:`metadata() <data.readers.grib.codes.GribField._metadata>`
 
 Examples:
 
diff --git a/docs/skip_api_rules.py b/docs/skip_api_rules.py
new file mode 100644
index 0000000..4b084f3
--- /dev/null
+++ b/docs/skip_api_rules.py
@@ -0,0 +1,99 @@
+# (C) Copyright 2020 ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+_skip_methods = {
+    "data.readers.bufr.bufr.BUFRList": [
+        "cache_file",
+        "dataset",
+        "from_dict",
+        "from_mask",
+        "from_multi",
+        "from_slice",
+        "full",
+        "graph",
+        "ignore",
+        "merge",
+        "mutate",
+        "new_mask_index",
+        "parent",
+        "settings",
+        "to_numpy",
+    ],
+    "data.readers.bufr.bufr.BUFRMessage": ["merge", "mutate", "to_numpy"],
+    "data.readers.grib.codes.GribField": ["merge", "mutate"],
+    "data.readers.grib.index.FieldList": [
+        "cache_file",
+        "dataset",
+        "from_dict",
+        "from_mask",
+        "from_multi",
+        "from_slice",
+        "full",
+        "graph",
+        "ignore",
+        "merge",
+        "mutate",
+        "new_mask_index",
+        "parent",
+        "scaled",
+        "settings",
+        "statistics",
+        "xarray_open_dataset_kwargs",
+    ],
+}
+
+
+# define skip rules for autoapi
+def _skip_api_items(app, what, name, obj, skip, options):
+    # if what == "package":
+    #     print(f"{name}[{what}]")
+
+    if what == "module" and name not in [
+        "data.readers",
+        "data.readers.bufr.bufr",
+        "data.readers.grib.codes",
+        "data.readers.grib.index",
+        "data.utils",
+        "data.utils.bbox",
+    ]:
+        skip = True
+    elif what == "package" and name not in [
+        "data",
+        "data.readers",
+        "data.readers.bufr",
+        "data.readers.bufr.bufr",
+        "data.readers.grib",
+        "data.readers.grib.index",
+        "data.utils",
+        "data.utils.bbox",
+    ]:
+        skip = True
+    elif what == "class" and name not in [
+        "data.readers.bufr.bufr.BUFRList",
+        "data.readers.bufr.bufr.BUFRMessage",
+        "data.readers.grib.codes.GribField",
+        "data.readers.grib.index.FieldList",
+        "data.utils.bbox.BoundingBox",
+    ]:
+        skip = True
+    elif what == "method":
+        if "abstractmethod" in getattr(obj, "properties", []):
+            skip = True
+        else:
+            for k, v in _skip_methods.items():
+                if k in name and name.split(".")[-1] in v:
+                    skip = True
+                    break
+
+    elif what in ("function", "attribute", "data"):
+        skip = True
+
+    # if not skip:
+    #     print(f"{what} {name}")
+    return skip
diff --git a/earthkit/data/core/index.py b/earthkit/data/core/index.py
index b83bd52..3cc9cd7 100644
--- a/earthkit/data/core/index.py
+++ b/earthkit/data/core/index.py
@@ -420,7 +420,7 @@ class Index(Source):
             original object, so no data is copied.
 
 
-        Ordering by a single metadata key ("param"). The default ordering directio
+        Ordering by a single metadata key ("param"). The default ordering direction
         is ``ascending``:
 
         >>> import earthkit.data
diff --git a/earthkit/data/readers/bufr/bufr.py b/earthkit/data/readers/bufr/bufr.py
index a7357ec..b89c054 100644
--- a/earthkit/data/readers/bufr/bufr.py
+++ b/earthkit/data/readers/bufr/bufr.py
@@ -142,6 +142,18 @@ class BUFRCodesReader(CodesReader):
 
 
 class BUFRMessage(Base):
+    r"""Represents a BUFR message in a BUFR file.
+
+    Parameters
+    ----------
+    path: str
+        Path to the BUFR file
+    offset: number
+        File offset of the message (in bytes)
+    length: number
+        Size of the message (in bytes)
+    """
+
     def __init__(self, path, offset, length):
         self.path = path
         self._offset = offset
@@ -169,12 +181,15 @@ class BUFRMessage(Base):
         return self.handle.get(key, default=None)
 
     def subset_count(self):
+        """Returns the number of subsets in the given BUFR message."""
         return self._header("numberOfSubsets")
 
     def is_compressed(self):
+        """Checks if the BUFR message contains compressed subsets."""
         return self.subset_count() > 1 and self._header("compressedData") == 1
 
     def is_uncompressed(self):
+        """Checks if the BUFR message contains uncompressed subsets."""
         return self.subset_count() > 1 and self._header("compressedData") == 0
 
     def __enter__(self):
@@ -198,14 +213,39 @@ class BUFRMessage(Base):
         """Returns an iterator for the keys the message contains."""
         return self.handle.__iter__()
 
+    def unpack(self):
+        """Decodes the data section of the message. When a message is unpacked all
+        the keys in the data section will be vailable via :obj:`metadata`.
+
+        See Also
+        --------
+        :obj:`unpack`
+        """
+        return self.handle.unpack()
+
+    def pack(self):
+        """Encodes the data section of the message. After calling ``pack`` the
+        contents of the data section will not be available and :obj:`metadata` will
+        able to access ``header keys`` only. To access the data section you need to use
+        :obj:`unpack` again.
+
+        See Also
+        --------
+        :obj:`unpack`
+        """
+        return self.handle.unpack()
+
     def metadata(self, *keys, astype=None, **kwargs):
-        r"""Returns metadata values from the BUFR message.
+        r"""Returns metadata values from the BUFR message. When the message in packed
+        (default state) only the header keys are available. To access data section keys
+        you need to call :obj:`unpack`.
 
         Parameters
         ----------
         *keys: tuple
             Positional arguments specifying metadata keys. Only ecCodes BUFR keys can be used
-            here. Can be empty, in this case all the keys will
+            here. It can contain a single str or a list or tuple. Can be empty, in this case
+            all the keys will
             be used.
         astype: type name, :obj:`list` or :obj:`tuple`
             Return types for ``keys``. A single value is accepted and applied to all the ``keys``.
@@ -226,13 +266,22 @@ class BUFRMessage(Base):
                 - single value when ``keys`` is a str
                 - otherwise the same type as that of ``keys`` (:obj:`list` or :obj:`tuple`)
             - when ``keys`` is empty:
-                - otherwise returns a :obj:`dict` with one item per key
+                - returns a :obj:`dict` with one item per key
 
         Raises
         ------
         KeyError
             If no ``default`` is set and a key is not found in the message or it has a missing value.
 
+        Examples
+        --------
+        >>> import earthkit.data
+        >>> ds = earthkit.data.from_source("file", "docs/examples/temp_10.bufr")
+        >>> ds[0].metadata("edition")
+        3
+        >>> ds[0].metadata("dataCategory", "dataSubCategory")
+        (2, 101)
+
         """
         key, namespace, astype, key_arg_type = metadata_argument(
             *keys, namespace=None, astype=astype
@@ -273,6 +322,24 @@ class BUFRMessage(Base):
             return False
 
     def dump(self, subset=1):
+        r"""Generates a dump with the message content represented as a tree view in a Jupyter notebook.
+
+        Parameters
+        ----------
+        subset: int
+            Subset to dump. Please note that susbset indexing starts at 1. Use None to dump all the
+            subsets in the message.
+
+        Returns
+        -------
+        HTML
+            Dump contents represented as a tree view in a Jupyter notebook.
+
+        Examples
+        --------
+        :ref:`/examples/bufr_temp.ipynb`
+
+        """
         from earthkit.data.core.temporary import temp_file
 
         with temp_file() as filename:
@@ -316,6 +383,48 @@ class BUFRMessage(Base):
 
 class BUFRListMixIn(PandasMixIn):
     def ls(self, *args, **kwargs):
+        r"""Generates a list like summary of the BUFR message list using a set of metadata keys.
+
+        Parameters
+        ----------
+        n: int, None
+            The number of :obj:`BUFRMEssage <data.readers.bufr.bufr.BUFRMessage>`\ s to be
+            listed. ``None`` means all the messages, ``n > 0`` means messages from the front, while
+            ``n < 0`` means messages from the back of the list.
+        keys: list of str, dict, None
+            Metadata keys. To specify a column title for each key in the output use a dict. If
+            ``keys`` is None the following dict will be used to define the titles and the keys::
+
+                {
+                    "edition": "edition",
+                    "type": "dataCategory",
+                    "subtype": "dataSubCategory",
+                    "c": "bufrHeaderCentre",
+                    "mv": "masterTablesVersionNumber",
+                    "lv": "localTablesVersionNumber",
+                    "subsets": "numberOfSubsets",
+                    "compr": "compressedData",
+                    "typicalDate": "typicalDate",
+                    "typicalTime": "typicalTime",
+                    "ident": "ident",
+                    "lat": "localLatitude",
+                    "lon": "localLongitude",
+                }
+
+        extra_keys: list of str, dict, None
+            List of additional keys to ``keys``. To specify a column title for each key in the output
+            use a dict.
+
+        Returns
+        -------
+        Pandas DataFrame
+            DataFrame with one row per :obj:`BUFRMEssage <data.readers.bufr.bufr.BUFRMessage>`.
+
+        Examples
+        --------
+        :ref:`/examples/bufr_temp.ipynb`
+
+        """
         from earthkit.data.utils.summary import ls
 
         def _proc(keys, n):
@@ -338,17 +447,100 @@ class BUFRListMixIn(PandasMixIn):
         return ls(_proc, BUFR_LS_KEYS, *args, **kwargs)
 
     def head(self, n=5, **kwargs):
+        r"""Generates a list like summary of the first ``n``
+        :obj:`BUFRMEssage <data.readers.bufr.bufr.BUFRMessage>`\ s using a set of metadata keys.
+        Same as calling :obj:`ls` with ``n``.
+
+        Parameters
+        ----------
+        n: int, None
+            The number of messages (``n`` > 0) to be printed from the front.
+        **kwargs: dict, optional
+            Other keyword arguments passed to :obj:`ls`.
+
+        Returns
+        -------
+        Pandas DataFrame
+            See  :obj:`ls`.
+
+
+        The following calls are equivalent:
+
+            .. code-block:: python
+
+                ds.head()
+                ds.head(5)
+                ds.head(n=5)
+                ds.ls(5)
+                ds.ls(n=5)
+
+        """
         if n <= 0:
             raise ValueError("head: n must be > 0")
         return self.ls(n=n, **kwargs)
 
     def tail(self, n=5, **kwargs):
+        r"""Generates a list like summary of the last ``n``
+        :obj:`BUFRMEssage <data.readers.bufr.bufr.BUFRMessage>`\ s using a set of metadata keys.
+        Same as calling :obj:`ls` with ``-n``.
+
+        Parameters
+        ----------
+        n: int, None
+            The number of messages (``n`` > 0)  to be printed from the back.
+        **kwargs: dict, optional
+            Other keyword arguments passed to :obj:`ls`.
+
+        Returns
+        -------
+        Pandas DataFrame
+            See  :obj:`ls`.
+
+
+        The following calls are equivalent:
+
+            .. code-block:: python
+
+                ds.tail()
+                ds.tail(5)
+                ds.tail(n=5)
+                ds.ls(-5)
+                ds.ls(n=-5)
+
+        """
         if n <= 0:
             raise ValueError("n must be > 0")
         return self.ls(n=-n, **kwargs)
 
+    def metadata(self, *args, **kwargs):
+        r"""Returns the metadata values for each message.
+
+        Parameters
+        ----------
+        *args: tuple
+            Positional arguments defining the metadata keys. Passed to
+            :obj:`BUFRMessage.metadata() <data.readers.bufr.bufr.BUFRMessage.metadata>`
+        **kwargs: dict, optional
+            Keyword arguments passed to
+            :obj:`BUFRMessage.metadata() <data.readers.bufr.bufr.BUFRMessage.metadata>`
+
+        Returns
+        -------
+        list
+            List with one item per :obj:`BUFRMessage <data.readers.bufr.bufr.BUFRMessage.metadata>`
+
+        """
+        result = []
+        for s in self:
+            result.append(s.metadata(*args, **kwargs))
+        return result
+
 
 class BUFRList(BUFRListMixIn, Index):
+    r"""Represents a list of
+    :obj:`BUFRMessage <data.readers.bufr.bufr.BUFRMessage>`\ s.
+    """
+
     def __init__(self, *args, **kwargs):
         Index.__init__(self, *args, **kwargs)
 
@@ -361,6 +553,72 @@ class BUFRList(BUFRListMixIn, Index):
         assert all(isinstance(_, BUFRList) for _ in sources)
         return MultiBUFRList(sources)
 
+    def sel(self, *args, **kwargs):
+        """Uses header metadata values to select only certain messages from
+          a BUFRList object.
+
+        Parameters
+        ----------
+        *args: tuple
+            Positional arguments specifying the filter condition as dict.
+            (See below for details).
+        **kwargs: dict, optional
+            Other keyword arguments specifying the filter conditions.
+            (See below for details).
+
+        Returns
+        -------
+        object
+            Returns a new object with the filtered elements. It contains a view to the data in the
+            original object, so no data is copied.
+
+
+        Filter conditions are specified by a set of **metadata** keys either by a dictionary (in
+        ``*args``) or a set of ``**kwargs``. Both single or multiple keys are allowed to use and each
+        can specify the following type of filter values:
+
+        - single value::
+
+            ds.sel(dataCategory="2")
+
+        - list of values::
+
+            ds.sel(dataCategory=[1, 2])
+
+        - **slice** of values (defines a **closed interval**, so treated as inclusive of both the start
+        and stop values, unlike normal Python indexing)::
+
+            # filter dataCategory between 1 and 4 inclusively
+            ds.sel(dataCategory=slice(1,4))
+        """
+        kwargs.pop("remapping", None)
+        return super().sel(*args, remapping=None, **kwargs)
+
+    def isel(self, *args, **kwargs):
+        raise NotImplementedError()
+
+    def order_by(self, *args, **kwargs):
+        """Changes the order of the messages in a BUFRList object.
+
+        Parameters
+        ----------
+        *args: tuple
+            Positional arguments specifying the metadata keys to perform the ordering on.
+            (See below for details)
+        **kwargs: dict, optional
+            Other keyword arguments specifying the metadata keys to perform the ordering on.
+            (See below for details)
+
+        Returns
+        -------
+        object
+            Returns a new object with reordered messages. It contains a view to the data in the
+            original object, so no data is copied.
+
+        """
+        kwargs.pop("remapping", None)
+        return super().order_by(*args, remapping=None, **kwargs)
+
 
 class MaskBUFRList(BUFRList, MaskIndex):
     def __init__(self, *args, **kwargs):
diff --git a/earthkit/data/readers/bufr/pandas.py b/earthkit/data/readers/bufr/pandas.py
index 8c72637..f91f612 100644
--- a/earthkit/data/readers/bufr/pandas.py
+++ b/earthkit/data/readers/bufr/pandas.py
@@ -35,8 +35,8 @@ class PandasMixIn:
 
         Examples
         --------
-        :ref:`/examples/bufr_temp.ipynb`
-        :ref:`/examples/bufr_synop.ipynb`
+        - :ref:`/examples/bufr_temp.ipynb`
+        - :ref:`/examples/bufr_synop.ipynb`
 
         """
         import pdbufr


Test patch:
diff --git a/tests/bufr/test_bufr_contents.py b/tests/bufr/test_bufr_contents.py
index e6f5519..4fdfcc2 100644
--- a/tests/bufr/test_bufr_contents.py
+++ b/tests/bufr/test_bufr_contents.py
@@ -31,6 +31,10 @@ def test_bufr_metadata():
     assert ds[0].is_compressed() is False
     assert ds[0].is_uncompressed() is False
 
+    assert ds[0].metadata("dataCategory") == 2
+
+    assert ds.metadata("dataCategory") == [2] * 10
+
 
 def test_bufr_metadata_uncompressed():
     ds = from_source(
diff --git a/tests/bufr/test_bufr_order_by.py b/tests/bufr/test_bufr_order_by.py
new file mode 100644
index 0000000..6bf2eda
--- /dev/null
+++ b/tests/bufr/test_bufr_order_by.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+
+# (C) Copyright 2020 ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import pytest
+
+from earthkit.data import from_source
+from earthkit.data.testing import earthkit_examples_file
+
+
+class _CustomOrder:
+    def __call__(self, x, y):
+        r = dict(t=2, u=0, v=1)
+        a = r[x]
+        b = r[y]
+        if a == b:
+            return 0
+        if a > b:
+            return 1
+        if a < b:
+            return -1
+
+
+@pytest.mark.parametrize(
+    "params,expected_meta",
+    [
+        (
+            "ident",
+            dict(
+                ident=[
+                    "01001",
+                    "01152",
+                    "01241",
+                    "01400",
+                    "01415",
+                    "02836",
+                    "02963",
+                    "03953",
+                    "11035",
+                    "11747",
+                ]
+            ),
+        ),
+    ],
+)
+def test_bufr_order_by_single_file(
+    params,
+    expected_meta,
+):
+    f = from_source("file", earthkit_examples_file("temp_10.bufr"))
+
+    g = f.order_by(params)
+    assert len(g) == len(f)
+
+    for k, v in expected_meta.items():
+        assert g.metadata(k) == v
+
+
+@pytest.mark.parametrize(
+    "params,expected_meta",
+    [
+        (
+            "ident",
+            dict(
+                ident=[
+                    "01001",
+                    "01152",
+                    "01400",
+                    "01415",
+                    "02836",
+                    "30823",
+                    "30846",
+                    "60545",
+                    "89514",
+                    "91648",
+                ]
+            ),
+        ),
+        (
+            ["ident"],
+            dict(
+                ident=[
+                    "01001",
+                    "01152",
+                    "01400",
+                    "01415",
+                    "02836",
+                    "30823",
+                    "30846",
+                    "60545",
+                    "89514",
+                    "91648",
+                ]
+            ),
+        ),
+        (
+            ["dataCategory", "ident"],
+            dict(
+                dataCategory=[0, 0, 0, 0, 0, 2, 2, 2, 2, 2],
+                ident=[
+                    "30823",
+                    "30846",
+                    "60545",
+                    "89514",
+                    "91648",
+                    "01001",
+                    "01152",
+                    "01400",
+                    "01415",
+                    "02836",
+                ],
+            ),
+        ),
+        (
+            dict(ident="ascending"),
+            dict(
+                ident=[
+                    "01001",
+                    "01152",
+                    "01400",
+                    "01415",
+                    "02836",
+                    "30823",
+                    "30846",
+                    "60545",
+                    "89514",
+                    "91648",
+                ]
+            ),
+        ),
+        (
+            dict(ident="descending"),
+            dict(
+                ident=[
+                    "91648",
+                    "89514",
+                    "60545",
+                    "30846",
+                    "30823",
+                    "02836",
+                    "01415",
+                    "01400",
+                    "01152",
+                    "01001",
+                ]
+            ),
+        ),
+    ],
+)
+def test_bufr_order_by_multi_file(params, expected_meta):
+    f1 = from_source("file", earthkit_examples_file("temp_10.bufr"))[:5]
+    f2 = from_source("file", earthkit_examples_file("synop_10.bufr"))[:5]
+    f = from_source("multi", [f1, f2])
+
+    g = f.order_by(params)
+    assert len(g) == len(f)
+
+    for k, v in expected_meta.items():
+        assert g.metadata(k) == v
+
+
+def test_bufr_order_by_with_sel():
+    f = from_source("file", earthkit_examples_file("temp_10.bufr"))
+
+    g = f.sel(ident=["01400", "01001", "11035"])
+    assert len(g) == 3
+    r = g.order_by("ident")
+    assert len(r) == len(g)
+    assert r.metadata("ident") == ["01001", "01400", "11035"]
+
+    g = f.sel(ident=["01400", "01001", "11035"])
+    assert len(g) == 3
+    r = g.order_by({"ident": "descending"})
+    assert len(r) == len(g)
+    assert r.metadata("ident") == ["11035", "01400", "01001"]
+
+
+if __name__ == "__main__":
+    from earthkit.data.testing import main
+
+    main()
diff --git a/tests/bufr/test_bufr_sel.py b/tests/bufr/test_bufr_sel.py
new file mode 100644
index 0000000..eb3dd3b
--- /dev/null
+++ b/tests/bufr/test_bufr_sel.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+
+# (C) Copyright 2020 ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import pytest
+
+from earthkit.data import from_source
+from earthkit.data.testing import earthkit_examples_file
+
+
+@pytest.mark.parametrize(
+    "params,expected_meta,metadata_keys",
+    [
+        (dict(dataCategory=2), [[2]] * 10, []),
+        (dict(dataCategory=2, ident="01400"), [[2, "01400"]], []),
+        (
+            dict(dataCategory=2, ident=["01400", "11747"]),
+            [
+                [2, "01400"],
+                [2, "11747"],
+            ],
+            [],
+        ),
+        (dict(dataCategory=22), [], []),
+        (dict(INVALIDKEY="w"), [], []),
+        (
+            dict(dataCategory=[2], ident=["01400", "11747"], dataSubCategory=101),
+            [
+                [2, "01400", 101],
+                [2, "11747", 101],
+            ],
+            [],
+        ),
+    ],
+)
+def test_bufr_sel_single_file_1(params, expected_meta, metadata_keys):
+    f = from_source("file", earthkit_examples_file("temp_10.bufr"))
+
+    g = f.sel(**params)
+    assert len(g) == len(expected_meta)
+    if len(expected_meta) > 0:
+        keys = list(params.keys())
+        if metadata_keys:
+            keys = metadata_keys
+
+        assert g.metadata(keys) == expected_meta
+    return
+
+
+def test_bufr_sel_single_file_as_dict():
+    f = from_source("file", earthkit_examples_file("temp_10.bufr"))
+    g = f.sel({"dataCategory": 2, "ident": ["01400", "11747"], "dataSubCategory": 101})
+    assert len(g) == 2
+    assert g.metadata(["dataCategory", "ident", "dataSubCategory"]) == [
+        [2, "01400", 101],
+        [2, "11747", 101],
+    ]
+
+
+def test_bufr_sel_multi_file():
+    f1 = from_source("file", earthkit_examples_file("temp_10.bufr"))
+    f2 = from_source("file", earthkit_examples_file("synop_10.bufr"))
+    f = from_source("multi", [f1, f2])
+
+    # single resulting message
+    g = f.sel(dataCategory=0, ident="68267")
+    assert len(g) == 1
+    assert g.metadata(["dataCategory", "ident:s"]) == [[0, "68267"]]
+
+
+if __name__ == "__main__":
+    from earthkit.data.testing import main
+
+    main()