Repository: ecmwf/earthkit-data
Pull request #124 (2023-06-20 07:57:52)

Implement bufr message and dump
This PR is implementing the BUFR message dump that allows inspecting the hierarchical contents of a given BUFR message in a Jupyter notebook (as a tree view).
Since the dump is related to a given message the concept of a BUFR message object and BUFR message list was introduced.

The current state of the BUFR interface is summarised below:

A BUFR file is represented by a `BUFRMessageList` object, each ecCodes BUFR message is represented by a `BUFRMessage`. Iteration, slicing, selection and data extraction work:

```python
ds = earthkit.data.from_source("file", "temp_10.bufr")

# this gives the number of messages
len(ds)

for f in ds:
    print(f)

# this is one BUFRMessage
ds[0]

# lists metadata contents
ds.ls()

# extraction
df = ds.to_pandas(columns=("WMO_station_id", "data_datetime", "pressure", "airTemperature"))
```

Methods on a given message:

```python
# dumps message contents
ds[0].dump()

# dumps a given subset if the message contains subsets
ds[0].dump(subset=2)

# number of subests
ds[0].subset_count()

# nature of subsets
ds[0].is_compressed()
ds[0].is_uncompressed()
```

Concatenation/multiple files works
```python
ds1 = earthkit.data.from_source("file", "temp_a.bufr")
ds2 = earthkit.data.from_source("file", "temp_b.bufr")
ds = ds1 + ds2
ds.ls()
```

Slicing, sel:
```python
ds = earthkit.data.from_source("file", "temp_10.bufr")
ds[1:3]
ds.sel("edition=4)
```

Limitations:
- to make `to_pandas`work on multiple files a new release of pdbufr is needed (pdbufr code is in a feature branch at the moment)

Questions:
- the repr of a BUFRMessage is not satisfactory and has to be improved. It looks like this at the moment:
 `BUFRMessage(type=0,subType=1,subsets=1,20230602,120000)`







Patch:
diff --git a/docs/examples/bufr_temp.ipynb b/docs/examples/bufr_temp.ipynb
index 68576e7..b1a0f5f 100644
--- a/docs/examples/bufr_temp.ipynb
+++ b/docs/examples/bufr_temp.ipynb
@@ -37,6 +37,14 @@
     "ds = earthkit.data.from_source(\"file\", \"temp_10.bufr\")"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "b07a6358-ad5a-4bd4-a4a3-c24c635cea39",
+   "metadata": {},
+   "source": [
+    "#### Message list"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": 3,
@@ -58,17 +66,38 @@
     "len(ds)"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "f360de9c-4584-4c78-a948-eaab7e45932d",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "BUFRMessage(type=2,subType=101,subsets=1,20081208,120000)"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ds[1]"
+   ]
+  },
   {
    "cell_type": "markdown",
    "id": "06d99f09-8d9d-4677-8aec-275787162a27",
    "metadata": {},
    "source": [
-    "We can call head(), tail() and ls() to see metadata from the header section of the BUFR messages: "
+    "We can call *head()*, *tail()* and *ls()* to see metadata from the header section of the BUFR messages: "
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 5,
    "id": "30e7749a-ea8b-43d2-90d2-31907f228513",
    "metadata": {},
    "outputs": [
@@ -209,7 +238,7 @@
        "4  01152  67.28  14.45  "
       ]
      },
-     "execution_count": 4,
+     "execution_count": 5,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -218,19 +247,736 @@
     "ds.head()"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "c9f29286-cf1c-4e3f-ba70-a715fcd81530",
+   "metadata": {},
+   "source": [
+    "#### Inspecting message contents"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7331e275-a157-407f-a9a0-5070db261d1d",
+   "metadata": {},
+   "source": [
+    "We can **dump** the contents of each message into a tree view. The dump below tells us that the message represents a given station and contains vertical profiles on a set of pressure levels (see the **data section**)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "613f01a9-8d0b-4dba-b74d-643aa40083ad",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<style type=\"text/css\">:root {\n",
+       "    --ekd-font-color0: var(--jp-content-font-color0, rgba(0, 0, 0, 1));\n",
+       "    --ekd-font-color2: var(--jp-content-font-color2, rgba(0, 0, 0, 0.54));\n",
+       "    --ekd-font-color3: var(--jp-content-font-color3, rgba(0, 0, 0, 0.38));\n",
+       "    --ekd-border-color: var(--jp-border-color2, #e0e0e0);\n",
+       "    --ekd-disabled-color: var(--jp-layout-color3, #bdbdbd);\n",
+       "    --ekd-background-color: var(--jp-layout-color0, white);\n",
+       "    --ekd-background-color-row-even: var(--jp-layout-color1, white);\n",
+       "    --ekd-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n",
+       "}\n",
+       "\n",
+       ".ekd-var-item>div,\n",
+       ".ekd-var-item label,\n",
+       ".ekd-var-item>.ekd-var-name span {\n",
+       "    background-color: var(--ekd-background-color-row-even);\n",
+       "    margin-bottom: 0;\n",
+       "}\n",
+       "\n",
+       ".ekd-var-item>.ekd-var-name:hover span {\n",
+       "    padding-right: 5px;\n",
+       "}\n",
+       "\n",
+       ".eh-description {\n",
+       "    color: var(--eh-font-color2);\n",
+       " }\n",
+       " \n",
+       ".ekd-sections {\n",
+       "    display: grid;\n",
+       "    grid-template-columns: repeat(3, auto);\n",
+       "    justify-content: start;\n",
+       "}\n",
+       "\n",
+       ".ekd-sections-1 {\n",
+       "    padding-left: 0 !important;\n",
+       "    display: grid;\n",
+       "    grid-template-columns: 150px auto auto 1fr 20px 20px;\n",
+       "}\n",
+       "\n",
+       ".ekd-var-name {\n",
+       "    grid-column: 1;\n",
+       "}\n",
+       "\n",
+       ".ekd-var-value {\n",
+       "    grid-column: 2;\n",
+       "}\n",
+       "\n",
+       ".ekd-var-units {\n",
+       "    grid-column: 3;\n",
+       "}\n",
+       "\n",
+       "li:nth-child(odd) {\n",
+       "    background-color: var(--ekd-background-color-row-odd);\n",
+       "}\n",
+       "\n",
+       ".tree {\n",
+       "    --spacing: 1rem;\n",
+       "    --radius: 5px;\n",
+       "}\n",
+       "\n",
+       ".tree li {\n",
+       "    display: block;\n",
+       "    position: relative;\n",
+       "    #padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);\n",
+       "}\n",
+       "\n",
+       ".tree ul {\n",
+       "    #margin-left: calc(var(--radius) - var(--spacing));\n",
+       "    padding-left: 0;\n",
+       "}\n",
+       "</style><div class=\"eh-description\">BUFRMessage(type=2,subType=101,subsets=1,20081208,120000)</div>\n",
+       "        <ul class=\"tree\">\n",
+       "        <li>\n",
+       "            \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>header</summary>\n",
+       "            <ul>\n",
+       "            <li>edition: 3</li><li>masterTableNumber: 0</li><li>bufrHeaderSubCentre: 0</li><li>bufrHeaderCentre: 98</li><li>updateSequenceNumber: 1</li><li>dataCategory: 2</li><li>dataSubCategory: 101</li><li>masterTablesVersionNumber: 13</li><li>localTablesVersionNumber: 1</li><li>typicalYearOfCentury: 8</li><li>typicalMonth: 12</li><li>typicalDay: 8</li><li>typicalHour: 12</li><li>typicalMinute: 0</li><li>typicalDate: 20081208</li><li>typicalTime: 120000</li><li>rdbType: 5</li><li>oldSubtype: 101</li><li>localYear: 2008</li><li>localMonth: 12</li><li>localDay: 8</li><li>localHour: 12</li><li>localMinute: 0</li><li>localSecond: 0</li><li>rdbtimeDay: 8</li><li>rdbtimeHour: 12</li><li>rdbtimeMinute: 39</li><li>rdbtimeSecond: 7</li><li>rdbtimeTime: 123907</li><li>rectimeDay: 8</li><li>rectimeHour: 12</li><li>rectimeMinute: 36</li><li>rectimeSecond: 29</li><li>restricted: 0</li><li>correction1: 0</li><li>correction1Part: 1</li><li>correction2: 0</li><li>correction2Part: 1</li><li>correction3: 0</li><li>correction3Part: 1</li><li>correction4: 0</li><li>correction4Part: 1</li><li>qualityControl: 70</li><li>newSubtype: 0</li><li>numberOfSubsets: 1</li><li>localLatitude: 56.9</li><li>localLongitude: 3.35</li><li>ident: 01400</li><li>observedData: 1</li><li>compressedData: 0</li><li>unexpandedDescriptors: array</li>\n",
+       "            </ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>data</summary>\n",
+       "            <ul>\n",
+       "            <li>subsetNumber: 1</li><li>blockNumber: 1 [Numeric]</li><li>stationNumber: 400 [Numeric]</li><li>radiosondeType: 80 [CODE TABLE]</li><li>radiosondeComputationalMethod: 4 [CODE TABLE]</li><li>year: 2008 [a]</li><li>month: 12 [mon]</li><li>day: 8 [d]</li><li>hour: 12 [h]</li><li>minute: 0 [min]</li><li>latitude: 56.9 [deg]</li><li>longitude: 3.35 [deg]</li><li>heightOfStation: 52 [m]</li><li>cloudCoverTotal: None [%]</li><li>verticalSignificanceSurfaceObservations: 1 [CODE TABLE]</li><li>cloudAmount: None [CODE TABLE]</li><li>heightOfBaseOfCloud: None [m]</li><li>cloudType: None [CODE TABLE]</li><li>cloudType: None [CODE TABLE]</li><li>cloudType: None [CODE TABLE]</li><li>delayedDescriptorReplicationFactor: 71 [Numeric]</li>\n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 100600</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 100600 [Pa]</li><li>verticalSoundingSignificance: 70 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 510 [m2 s-2]</li><li>airTemperature: 281.2 [K]</li><li>dewpointTemperature: 278.4 [K]</li><li>windDirection: 300 [deg]</li><li>windSpeed: 10 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 100500</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 100500 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 590 [m2 s-2]</li><li>airTemperature: 281 [K]</li><li>dewpointTemperature: 271 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 100000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 100000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 1000 [m2 s-2]</li><li>airTemperature: 280.6 [K]</li><li>dewpointTemperature: 275.6 [K]</li><li>windDirection: 300 [deg]</li><li>windSpeed: 10 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 99800</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 99800 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 1150 [m2 s-2]</li><li>airTemperature: 280.4 [K]</li><li>dewpointTemperature: 276.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 93200</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 93200 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 6620 [m2 s-2]</li><li>airTemperature: 275 [K]</li><li>dewpointTemperature: 274.3 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 92500</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 92500 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 7220 [m2 s-2]</li><li>airTemperature: 275 [K]</li><li>dewpointTemperature: 272.1 [K]</li><li>windDirection: 295 [deg]</li><li>windSpeed: 10 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 89400</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 89400 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 9910 [m2 s-2]</li><li>airTemperature: 273.6 [K]</li><li>dewpointTemperature: 266.6 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 88900</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 88900 [Pa]</li><li>verticalSoundingSignificance: 6 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 285 [deg]</li><li>windSpeed: 12 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 85800</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 85800 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 13120 [m2 s-2]</li><li>airTemperature: 270.9 [K]</li><li>dewpointTemperature: 268.7 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 85200</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 85200 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 13670 [m2 s-2]</li><li>airTemperature: 270.9 [K]</li><li>dewpointTemperature: 263.9 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 85000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 85000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 13870 [m2 s-2]</li><li>airTemperature: 270.7 [K]</li><li>dewpointTemperature: 264.7 [K]</li><li>windDirection: 280 [deg]</li><li>windSpeed: 14 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 82000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 82000 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 16640 [m2 s-2]</li><li>airTemperature: 268.7 [K]</li><li>dewpointTemperature: 268.2 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 80000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 80000 [Pa]</li><li>verticalSoundingSignificance: 6 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 270 [deg]</li><li>windSpeed: 16 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 76700</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 76700 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 21760 [m2 s-2]</li><li>airTemperature: 265.3 [K]</li><li>dewpointTemperature: 260.8 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 75400</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 75400 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 23070 [m2 s-2]</li><li>airTemperature: 264.5 [K]</li><li>dewpointTemperature: 261.9 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 75200</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 75200 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 23270 [m2 s-2]</li><li>airTemperature: 264.5 [K]</li><li>dewpointTemperature: 261.9 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 74500</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 74500 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 23980 [m2 s-2]</li><li>airTemperature: 265.1 [K]</li><li>dewpointTemperature: 249.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 74400</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 74400 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 255 [deg]</li><li>windSpeed: 20 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 73600</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 73600 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 24900 [m2 s-2]</li><li>airTemperature: 265.1 [K]</li><li>dewpointTemperature: 249.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 73100</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 73100 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 25420 [m2 s-2]</li><li>airTemperature: 265.1 [K]</li><li>dewpointTemperature: 232.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 70000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 70000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 28720 [m2 s-2]</li><li>airTemperature: 263.1 [K]</li><li>dewpointTemperature: 222.1 [K]</li><li>windDirection: 260 [deg]</li><li>windSpeed: 21 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 64600</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 64600 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 34750 [m2 s-2]</li><li>airTemperature: 260.9 [K]</li><li>dewpointTemperature: 214.9 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 64300</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 64300 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 35100 [m2 s-2]</li><li>airTemperature: 261.1 [K]</li><li>dewpointTemperature: 215.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 62400</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 62400 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 37340 [m2 s-2]</li><li>airTemperature: 259.7 [K]</li><li>dewpointTemperature: 221.7 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 60900</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 60900 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 255 [deg]</li><li>windSpeed: 29 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 60000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 60000 [Pa]</li><li>verticalSoundingSignificance: 6 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 255 [deg]</li><li>windSpeed: 29 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 59800</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 59800 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 40500 [m2 s-2]</li><li>airTemperature: 256.9 [K]</li><li>dewpointTemperature: 254.5 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 59400</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 59400 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 40990 [m2 s-2]</li><li>airTemperature: 256.5 [K]</li><li>dewpointTemperature: 252.2 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 59200</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 59200 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 41240 [m2 s-2]</li><li>airTemperature: 256.5 [K]</li><li>dewpointTemperature: 246.5 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 58800</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 58800 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 41740 [m2 s-2]</li><li>airTemperature: 256.3 [K]</li><li>dewpointTemperature: 231.3 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 55600</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 55600 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 45830 [m2 s-2]</li><li>airTemperature: 253.1 [K]</li><li>dewpointTemperature: 231.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 55300</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 55300 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 46220 [m2 s-2]</li><li>airTemperature: 253.1 [K]</li><li>dewpointTemperature: 232.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 55100</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 55100 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 46490 [m2 s-2]</li><li>airTemperature: 253.1 [K]</li><li>dewpointTemperature: 245.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 54200</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 54200 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 47680 [m2 s-2]</li><li>airTemperature: 251.9 [K]</li><li>dewpointTemperature: 247.6 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 51900</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 51900 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 50800 [m2 s-2]</li><li>airTemperature: 249.5 [K]</li><li>dewpointTemperature: 227.5 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 50200</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 50200 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 53180 [m2 s-2]</li><li>airTemperature: 248.1 [K]</li><li>dewpointTemperature: 228.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 50000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 50000 [Pa]</li><li>verticalSoundingSignificance: 36 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 53450 [m2 s-2]</li><li>airTemperature: 248.1 [K]</li><li>dewpointTemperature: 237.1 [K]</li><li>windDirection: 255 [deg]</li><li>windSpeed: 32 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 49000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 49000 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 54900 [m2 s-2]</li><li>airTemperature: 246.9 [K]</li><li>dewpointTemperature: 232.9 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 43000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 43000 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 63990 [m2 s-2]</li><li>airTemperature: 238.3 [K]</li><li>dewpointTemperature: 235.3 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 40000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 40000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 68940 [m2 s-2]</li><li>airTemperature: 234.3 [K]</li><li>dewpointTemperature: 230.1 [K]</li><li>windDirection: 245 [deg]</li><li>windSpeed: 34 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 39400</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 39400 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 245 [deg]</li><li>windSpeed: 34 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 37000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 37000 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 74090 [m2 s-2]</li><li>airTemperature: 229.7 [K]</li><li>dewpointTemperature: 224.7 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 35300</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 35300 [Pa]</li><li>verticalSoundingSignificance: 10 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 250 [deg]</li><li>windSpeed: 38 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 34600</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 34600 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 78470 [m2 s-2]</li><li>airTemperature: 225.7 [K]</li><li>dewpointTemperature: 222.7 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 32100</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 32100 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 83290 [m2 s-2]</li><li>airTemperature: 222.3 [K]</li><li>dewpointTemperature: 214.3 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 30700</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 30700 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 270 [deg]</li><li>windSpeed: 33 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 30300</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 30300 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 86940 [m2 s-2]</li><li>airTemperature: 219.1 [K]</li><li>dewpointTemperature: 214.8 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 30000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 30000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 87570 [m2 s-2]</li><li>airTemperature: 218.7 [K]</li><li>dewpointTemperature: 214.3 [K]</li><li>windDirection: 270 [deg]</li><li>windSpeed: 32 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 28700</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 28700 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 90340 [m2 s-2]</li><li>airTemperature: 217.3 [K]</li><li>dewpointTemperature: 212.3 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 27900</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 27900 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 92110 [m2 s-2]</li><li>airTemperature: 217.9 [K]</li><li>dewpointTemperature: 206.9 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 25000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 25000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 98950 [m2 s-2]</li><li>airTemperature: 215.1 [K]</li><li>dewpointTemperature: 204.1 [K]</li><li>windDirection: 270 [deg]</li><li>windSpeed: 31 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 23500</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 23500 [Pa]</li><li>verticalSoundingSignificance: 20 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 102730 [m2 s-2]</li><li>airTemperature: 213.5 [K]</li><li>dewpointTemperature: 199.5 [K]</li><li>windDirection: 265 [deg]</li><li>windSpeed: 27 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 22100</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 22100 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 260 [deg]</li><li>windSpeed: 24 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 21000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 21000 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 109610 [m2 s-2]</li><li>airTemperature: 212.9 [K]</li><li>dewpointTemperature: 194.9 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 20000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 20000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 112580 [m2 s-2]</li><li>airTemperature: 212.1 [K]</li><li>dewpointTemperature: 194.1 [K]</li><li>windDirection: 255 [deg]</li><li>windSpeed: 25 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 15000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 15000 [Pa]</li><li>verticalSoundingSignificance: 36 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 130230 [m2 s-2]</li><li>airTemperature: 214.1 [K]</li><li>dewpointTemperature: 186.1 [K]</li><li>windDirection: 275 [deg]</li><li>windSpeed: 22 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 14100</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 14100 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 275 [deg]</li><li>windSpeed: 22 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 12100</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 12100 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 265 [deg]</li><li>windSpeed: 20 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 11200</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 11200 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 275 [deg]</li><li>windSpeed: 18 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 10000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 10000 [Pa]</li><li>verticalSoundingSignificance: 38 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 155040 [m2 s-2]</li><li>airTemperature: 211.1 [K]</li><li>dewpointTemperature: 182.1 [K]</li><li>windDirection: 290 [deg]</li><li>windSpeed: 22 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 8340</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 8340 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 280 [deg]</li><li>windSpeed: 12 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 7140</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 7140 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 175120 [m2 s-2]</li><li>airTemperature: 206.1 [K]</li><li>dewpointTemperature: 180.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 7000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 7000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 176420 [m2 s-2]</li><li>airTemperature: 207.9 [K]</li><li>dewpointTemperature: 180.9 [K]</li><li>windDirection: 275 [deg]</li><li>windSpeed: 24 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 6810</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 6810 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 275 [deg]</li><li>windSpeed: 26 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 6050</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 6050 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 185060 [m2 s-2]</li><li>airTemperature: 212.1 [K]</li><li>dewpointTemperature: 182.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 6020</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 6020 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 270 [deg]</li><li>windSpeed: 21 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 5630</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 5630 [Pa]</li><li>verticalSoundingSignificance: 2 [FLAG TABLE]</li><li>nonCoordinateGeopotential: None [m2 s-2]</li><li>airTemperature: None [K]</li><li>dewpointTemperature: None [K]</li><li>windDirection: 245 [deg]</li><li>windSpeed: 17 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 5080</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 5080 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 195570 [m2 s-2]</li><li>airTemperature: 207.1 [K]</li><li>dewpointTemperature: 179.1 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 5000</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 5000 [Pa]</li><li>verticalSoundingSignificance: 32 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 196620 [m2 s-2]</li><li>airTemperature: 207.1 [K]</li><li>dewpointTemperature: 178.1 [K]</li><li>windDirection: 245 [deg]</li><li>windSpeed: 22 [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 4810</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 4810 [Pa]</li><li>verticalSoundingSignificance: 4 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 198810 [m2 s-2]</li><li>airTemperature: 206.5 [K]</li><li>dewpointTemperature: 178.5 [K]</li><li>windDirection: None [deg]</li><li>windSpeed: None [m/s]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 4560</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 4560 [Pa]</li><li>verticalSoundingSignificance: 6 [FLAG TABLE]</li><li>nonCoordinateGeopotential: 201980 [m2 s-2]</li><li>airTemperature: 207.5 [K]</li><li>dewpointTemperature: 179.5 [K]</li><li>windDirection: 255 [deg]</li><li>windSpeed: 28 [m/s]</li><li>delayedDescriptorReplicationFactor: 1 [Numeric]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        <li>\n",
+       "        <details>\n",
+       "            <summary>pressure: 35300</summary>\n",
+       "            <ul>\n",
+       "        <li>pressure: 35300 [Pa]</li><li>verticalSoundingSignificance: 8 [FLAG TABLE]</li><li>absoluteWindShearIn1KmLayerBelow: 5 [m/s]</li><li>absoluteWindShearIn1KmLayerAbove: 12 [m/s]</li><li>operator: 0</li><li>extendedDelayedDescriptorReplicationFactor: 522 [Numeric]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 1 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>dataPresentIndicator: 0 [FLAG TABLE]</li><li>centre: 98 [CODE TABLE]</li><li>generatingApplication: 1 [CODE TABLE]</li><li>extendedDelayedDescriptorReplicationFactor: 391 [Numeric]</li></ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "            </ul>\n",
+       "            </details>\n",
+       "        </li>\n",
+       "        \n",
+       "        </li>\n",
+       "        </ul>\n",
+       "        "
+      ],
+      "text/plain": [
+       "<IPython.core.display.HTML object>"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ds[1].dump()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c0f22bfb-9e5f-4a69-ad87-b4d26fd7fb27",
+   "metadata": {},
+   "source": [
+    "#### Converting to Pandas"
+   ]
+  },
   {
    "cell_type": "markdown",
    "id": "07ca0ea4-cad9-4973-ac79-681f9140c116",
    "metadata": {},
    "source": [
-    "BUFR data can be extracted into a Pandas dataframe using [to_pandas()](https://earthkit-data.readthedocs.io/en/latest/guide/data_format/bufr.html), which passes all the arguments to the [read_bufr()](https://pdbufr.readthedocs.io/en/latest/read_bufr.html) method from [pdbufr](\"https://github.com/ecmwf/pdbufr\")\n",
+    "BUFR data can be extracted into a Pandas dataframe using [to_pandas()](https://earthkit-data.readthedocs.io/en/latest/guide/data_format/bufr.html), which passes all the arguments to the [read_bufr()](https://pdbufr.readthedocs.io/en/latest/read_bufr.html) method from [pdbufr](\"https://github.com/ecmwf/pdbufr\").\n",
     "\n",
-    "The following example shows how the get the temperature profile for a given station:"
+    "The following example shows how the get the temperature profile for a given station. Please note that \"WMO_station_id\" and \"data_datetime\" are generated keys and not present in the BUFR messages themselves."
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "id": "53ac87d6-09b9-46c3-bc54-07fd2a175b05",
    "metadata": {},
    "outputs": [
@@ -361,7 +1107,7 @@
        "[63 rows x 4 columns]"
       ]
      },
-     "execution_count": 5,
+     "execution_count": 7,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -397,7 +1143,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.16"
+   "version": "3.8.17"
   }
  },
  "nbformat": 4,
diff --git a/earthkit/data/core/index.py b/earthkit/data/core/index.py
index 2ebe5f4..b83bd52 100644
--- a/earthkit/data/core/index.py
+++ b/earthkit/data/core/index.py
@@ -188,7 +188,7 @@ class Index(Source):
     def __len__(self):
         self._not_implemented()
 
-    def _normalize_kwargs_names(**kwargs):
+    def _normalize_kwargs_names(self, **kwargs):
         return kwargs
 
     def sel(self, *args, remapping=None, **kwargs):
diff --git a/earthkit/data/data/css/tree.css b/earthkit/data/data/css/tree.css
new file mode 100644
index 0000000..cbae8ca
--- /dev/null
+++ b/earthkit/data/data/css/tree.css
@@ -0,0 +1,69 @@
+:root {
+    --ekd-font-color0: var(--jp-content-font-color0, rgba(0, 0, 0, 1));
+    --ekd-font-color2: var(--jp-content-font-color2, rgba(0, 0, 0, 0.54));
+    --ekd-font-color3: var(--jp-content-font-color3, rgba(0, 0, 0, 0.38));
+    --ekd-border-color: var(--jp-border-color2, #e0e0e0);
+    --ekd-disabled-color: var(--jp-layout-color3, #bdbdbd);
+    --ekd-background-color: var(--jp-layout-color0, white);
+    --ekd-background-color-row-even: var(--jp-layout-color1, white);
+    --ekd-background-color-row-odd: var(--jp-layout-color2, #eeeeee);
+}
+
+.ekd-var-item>div,
+.ekd-var-item label,
+.ekd-var-item>.ekd-var-name span {
+    background-color: var(--ekd-background-color-row-even);
+    margin-bottom: 0;
+}
+
+.ekd-var-item>.ekd-var-name:hover span {
+    padding-right: 5px;
+}
+
+.eh-description {
+    color: var(--eh-font-color2);
+ }
+
+.ekd-sections {
+    display: grid;
+    grid-template-columns: repeat(3, auto);
+    justify-content: start;
+}
+
+.ekd-sections-1 {
+    padding-left: 0 !important;
+    display: grid;
+    grid-template-columns: 150px auto auto 1fr 20px 20px;
+}
+
+.ekd-var-name {
+    grid-column: 1;
+}
+
+.ekd-var-value {
+    grid-column: 2;
+}
+
+.ekd-var-units {
+    grid-column: 3;
+}
+
+li:nth-child(odd) {
+    background-color: var(--ekd-background-color-row-odd);
+}
+
+.tree {
+    --spacing: 1rem;
+    --radius: 5px;
+}
+
+.tree li {
+    display: block;
+    position: relative;
+    #padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
+}
+
+.tree ul {
+    #margin-left: calc(var(--radius) - var(--spacing));
+    padding-left: 0;
+}
diff --git a/earthkit/data/readers/bufr.py b/earthkit/data/readers/bufr.py
deleted file mode 100644
index 97cfad0..0000000
--- a/earthkit/data/readers/bufr.py
+++ /dev/null
@@ -1,116 +0,0 @@
-# (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.
-#
-
-# See:
-# https://github.com/ecmwf/pdbufr
-from . import Reader
-
-COLUMNS = ("latitude", "longitude", "data_datetime")
-
-BUFR_LS_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",
-}
-
-
-class BUFRReader(Reader):
-    """Represents a BUFR file"""
-
-    def __init__(self, source, path):
-        super().__init__(source, path)
-        self._num = None
-
-    def to_pandas(self, columns=COLUMNS, filters=None, **kwargs):
-        """Extracts BUFR data into an pandas DataFranme using :xref:`pdbufr`.
-
-        Parameters
-        ----------
-        columns: str, sequence[str]
-            List of ecCodes BUFR keys to extract for each BUFR message/subset.
-            See: :xref:`read_bufr` for details.
-        filters: dict
-            Defines the conditions when to extract the specified ``columns``. See:
-            :xref:`read_bufr` for details.
-        **kwargs: dict, optional
-            Other keyword arguments:
-
-        Returns
-        -------
-        Pandas DataFrame
-
-        Examples
-        --------
-        :ref:`/examples/bufr.ipynb`
-
-        """
-        import pdbufr
-
-        filters = {} if filters is None else filters
-        return pdbufr.read_bufr(self.path, columns=columns, filters=filters)
-
-    def __len__(self):
-        if self._num is None:
-            import eccodes
-
-            with open(self.path, "rb") as f:
-                self._num = eccodes.codes_count_in_file(f)
-        return self._num
-
-    def ls(self, *args, **kwargs):
-        from pdbufr.high_level_bufr.bufr import BufrFile
-
-        from earthkit.data.utils.summary import ls
-
-        b = BufrFile(self.path)
-
-        def _proc(keys, n):
-            count_start = 0
-            if n is None:
-                count_end = len(self)
-            elif n > 0:
-                count_end = n
-            else:
-                num = len(self)
-                count_start = max(0, num + n)
-                count_end = num
-
-            for count, msg in enumerate(b):
-                with msg:
-                    if count_start <= count < count_end:
-                        yield ({k: msg[k1] for k, k1 in keys.items()})
-                    elif count >= count_end:
-                        break
-
-        return ls(_proc, BUFR_LS_KEYS, *args, **kwargs)
-
-
-def _match_magic(magic, deeper_check):
-    if magic is not None:
-        type_id = b"BUFR"
-        if not deeper_check:
-            return len(magic) >= 4 and magic[:4] == type_id
-        else:
-            return type_id in magic
-    return False
-
-
-def reader(source, path, magic=None, deeper_check=False):
-    if _match_magic(magic, deeper_check):
-        return BUFRReader(source, path)
diff --git a/earthkit/data/readers/bufr/__init__.py b/earthkit/data/readers/bufr/__init__.py
new file mode 100644
index 0000000..4327988
--- /dev/null
+++ b/earthkit/data/readers/bufr/__init__.py
@@ -0,0 +1,28 @@
+# (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 logging
+
+LOG = logging.getLogger(__name__)
+
+
+def _match_magic(magic, deeper_check):
+    if magic is not None:
+        type_id = b"BUFR"
+        if not deeper_check:
+            return len(magic) >= 4 and magic[:4] == type_id
+        else:
+            return type_id in magic
+    return False
+
+
+def reader(source, path, magic=None, deeper_check=False):
+    if _match_magic(magic, deeper_check):
+        from .bufr import BUFRReader
+
+        return BUFRReader(source, path)
diff --git a/earthkit/data/readers/bufr/bufr.py b/earthkit/data/readers/bufr/bufr.py
new file mode 100644
index 0000000..a7357ec
--- /dev/null
+++ b/earthkit/data/readers/bufr/bufr.py
@@ -0,0 +1,438 @@
+# (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 os
+from abc import abstractmethod
+
+import eccodes
+from pdbufr.high_level_bufr.bufr import bufr_code_is_coord
+
+from earthkit.data.core import Base
+from earthkit.data.core.index import Index, MaskIndex, MultiIndex
+from earthkit.data.utils.message import (
+    CodesHandle,
+    CodesMessagePositionIndex,
+    CodesReader,
+)
+from earthkit.data.utils.metadata import metadata_argument
+from earthkit.data.utils.parts import Part
+from earthkit.data.utils.summary import make_bufr_html_tree
+
+from .. import Reader
+from .pandas import PandasMixIn
+
+BUFR_LS_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",
+}
+
+
+class BufrCodesMessagePositionIndex(CodesMessagePositionIndex):
+    # This does not belong here, should be in the C library
+    def _get_message_positions(self, path):
+        fd = os.open(path, os.O_RDONLY)
+        try:
+
+            def get(count):
+                buf = os.read(fd, count)
+                assert len(buf) == count
+                return int.from_bytes(
+                    buf,
+                    byteorder="big",
+                    signed=False,
+                )
+
+            offset = 0
+            while True:
+                code = os.read(fd, 4)
+                if len(code) < 4:
+                    break
+
+                if code != b"BUFR":
+                    offset = os.lseek(fd, offset + 1, os.SEEK_SET)
+                    continue
+
+                length = get(3)
+                edition = get(1)
+
+                if edition in [3, 4]:
+                    yield offset, length
+                    offset = os.lseek(fd, offset + length, os.SEEK_SET)
+
+        finally:
+            os.close(fd)
+
+
+class BUFRCodesHandle(CodesHandle):
+    PRODUCT_ID = eccodes.CODES_PRODUCT_BUFR
+
+    def __init__(self, handle, path, offset):
+        super().__init__(handle, path, offset)
+        self._unpacked = False
+
+    def unpack(self):
+        """Decode data section"""
+        if not self._unpacked:
+            eccodes.codes_set(self._handle, "unpack", 1)
+            self._unpacked = True
+
+    def pack(self):
+        """Encode data section"""
+        if self._unpacked:
+            eccodes.codes_set(self._handle, "pack", 1)
+            self._unpacked = False
+
+    def json_dump(self, path):
+        self.unpack()
+        with open(path, "w") as f:
+            eccodes.codes_dump(self._handle, f, "json")
+        self.pack()
+
+    def __iter__(self):
+        class _KeyIterator:
+            def __init__(self, handle):
+                self._iterator = eccodes.codes_bufr_keys_iterator_new(handle)
+
+            def __del__(self):
+                try:
+                    eccodes.codes_bufr_keys_iterator_delete(self._iterator)
+                except Exception:
+                    pass
+
+            def __iter__(self):
+                return self
+
+            def __next__(self):
+                while True:
+                    if not eccodes.codes_bufr_keys_iterator_next(self._iterator):
+                        raise StopIteration
+
+                    return eccodes.codes_bufr_keys_iterator_get_name(self._iterator)
+
+        return _KeyIterator(self._handle)
+
+    def keys(self, namespace=None):
+        """Iterate over all the available keys"""
+        return self.__iter__()
+
+    def as_namespace(self, namespace=None):
+        return {k: self.get(k) for k in self.keys(namespace=namespace)}
+
+
+class BUFRCodesReader(CodesReader):
+    PRODUCT_ID = eccodes.CODES_PRODUCT_BUFR
+    HANDLE_TYPE = BUFRCodesHandle
+
+
+class BUFRMessage(Base):
+    def __init__(self, path, offset, length):
+        self.path = path
+        self._offset = offset
+        self._length = length
+        self._handle = None
+
+    @property
+    def handle(self):
+        r""":class:`CodesHandle`: Gets an object providing access to the low level BUFR message structure."""
+        if self._handle is None:
+            assert self._offset is not None
+            self._handle = BUFRCodesReader.from_cache(self.path).at_offset(self._offset)
+        return self._handle
+
+    def __repr__(self):
+        return "BUFRMessage(type=%s,subType=%s,subsets=%s,%s,%s)" % (
+            self.handle.get("dataCategory", default=None),
+            self.handle.get("dataSubCategory", default=None),
+            self.handle.get("numberOfSubsets", default=None),
+            self.handle.get("typicalDate", default=None),
+            self.handle.get("typicalTime", default=None),
+        )
+
+    def _header(self, key):
+        return self.handle.get(key, default=None)
+
+    def subset_count(self):
+        return self._header("numberOfSubsets")
+
+    def is_compressed(self):
+        return self.subset_count() > 1 and self._header("compressedData") == 1
+
+    def is_uncompressed(self):
+        return self.subset_count() > 1 and self._header("compressedData") == 0
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self._handle = None
+
+    def __setitem__(self, key, value):
+        """Sets value associated with ``key``"""
+        if isinstance(value, list):
+            return eccodes.codes_set_array(self.handle._handle, key, value)
+        else:
+            return eccodes.codes_set(self.handle._handle, key, value)
+
+    def __getitem__(self, key):
+        """Returns the value of the ``key``."""
+        return self.handle.get(key)
+
+    def __iter__(self):
+        """Returns an iterator for the keys the message contains."""
+        return self.handle.__iter__()
+
+    def metadata(self, *keys, astype=None, **kwargs):
+        r"""Returns metadata values from the BUFR message.
+
+        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
+            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``.
+            Otherwise, must have same the number of elements as ``keys``. Only used when
+            ``keys`` is not empty.
+        **kwargs: tuple, optional
+            Other keyword arguments:
+
+            * default: value, optional
+                Specifies the same default value for all the ``keys`` specified. When ``default`` is
+                **not present** and a key is not found or its value is a missing value
+                :obj:`metadata` will raise KeyError.
+
+        Returns
+        -------
+        single value, :obj:`list`, :obj:`tuple` or :obj:`dict`
+            - when ``keys`` is not empty:
+                - 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
+
+        Raises
+        ------
+        KeyError
+            If no ``default`` is set and a key is not found in the message or it has a missing value.
+
+        """
+        key, namespace, astype, key_arg_type = metadata_argument(
+            *keys, namespace=None, astype=astype
+        )
+
+        assert isinstance(key, list)
+        assert isinstance(namespace, (list, tuple))
+
+        if key:
+            assert isinstance(astype, (list, tuple))
+            r = [self.handle.get(k, ktype=kt, **kwargs) for k, kt in zip(key, astype)]
+
+            if key_arg_type == str:
+                return r[0]
+            elif key_arg_type == tuple:
+                return tuple(r)
+            else:
+                return r
+        else:
+            return self.handle.as_namespace()
+
+    def is_coord(self, key):
+        """Check if the specified key is a BUFR coordinate descriptor
+
+        Parameters
+        ----------
+        key: str
+            Key name (can contain ecCodes rank)
+
+        Returns
+        -------
+        bool
+            True if the specified ``key`` is a BUFR coordinate descriptor
+        """
+        try:
+            return bufr_code_is_coord(self.d[key + "->code"])
+        except Exception:
+            return False
+
+    def dump(self, subset=1):
+        from earthkit.data.core.temporary import temp_file
+
+        with temp_file() as filename:
+            self.handle.json_dump(filename)
+            with open(filename, "r") as f:
+                import json
+                import warnings
+
+                try:
+                    d = json.loads(f.read())
+                    return make_bufr_html_tree(
+                        d,
+                        self.__repr__(),
+                        subset,
+                        self.is_compressed(),
+                        self.is_uncompressed(),
+                    )
+                except Exception as e:
+                    warnings.warn("Failed to parse bufr_dump", e)
+                    return None
+
+    def write(self, f):
+        r"""Writes the message to a file object.
+
+        Parameters
+        ----------
+        f: file object
+            The target file object.
+        """
+        self.handle.write_to(f)
+
+    def message(self):
+        r"""Returns a buffer containing the encoded message.
+
+        Returns
+        -------
+        bytes
+        """
+        return self.handle.get_buffer()
+
+
+class BUFRListMixIn(PandasMixIn):
+    def ls(self, *args, **kwargs):
+        from earthkit.data.utils.summary import ls
+
+        def _proc(keys, n):
+            count_start = 0
+            if n is None:
+                count_end = len(self)
+            elif n > 0:
+                count_end = n
+            else:
+                num = len(self)
+                count_start = max(0, num + n)
+                count_end = num
+
+            for count, msg in enumerate(self):
+                if count_start <= count < count_end:
+                    yield ({k: msg._header(k1) for k, k1 in keys.items()})
+                elif count >= count_end:
+                    break
+
+        return ls(_proc, BUFR_LS_KEYS, *args, **kwargs)
+
+    def head(self, n=5, **kwargs):
+        if n <= 0:
+            raise ValueError("head: n must be > 0")
+        return self.ls(n=n, **kwargs)
+
+    def tail(self, n=5, **kwargs):
+        if n <= 0:
+            raise ValueError("n must be > 0")
+        return self.ls(n=-n, **kwargs)
+
+
+class BUFRList(BUFRListMixIn, Index):
+    def __init__(self, *args, **kwargs):
+        Index.__init__(self, *args, **kwargs)
+
+    @classmethod
+    def new_mask_index(self, *args, **kwargs):
+        return MaskBUFRList(*args, **kwargs)
+
+    @classmethod
+    def merge(cls, sources):
+        assert all(isinstance(_, BUFRList) for _ in sources)
+        return MultiBUFRList(sources)
+
+
+class MaskBUFRList(BUFRList, MaskIndex):
+    def __init__(self, *args, **kwargs):
+        MaskIndex.__init__(self, *args, **kwargs)
+
+
+class MultiBUFRList(BUFRList, MultiIndex):
+    def __init__(self, *args, **kwargs):
+        MultiIndex.__init__(self, *args, **kwargs)
+
+
+class BUFRInFiles(BUFRList):
+    # Remote BUFRLists (with urls) are also here,
+    # as the actual fieldlist is accessed on a file in cache.
+    # This class changes the interface (_getitem__ and __len__)
+    # into the interface (part and number_of_parts).
+    def __getitem__(self, n):
+        if isinstance(n, int):
+            part = self.part(n if n >= 0 else len(self) + n)
+            return BUFRMessage(part.path, part.offset, part.length)
+        else:
+            return super().__getitem__(n)
+
+    def __len__(self):
+        return self.number_of_parts()
+
+    @abstractmethod
+    def part(self, n):
+        self._not_implemented()
+
+    @abstractmethod
+    def number_of_parts(self):
+        self._not_implemented()
+
+
+class BUFRInOneFile(BUFRInFiles):
+    def __init__(self, path):
+        self.path = path
+        self.__positions = None
+
+    @property
+    def _positions(self):
+        if self.__positions is None:
+            self.__positions = BufrCodesMessagePositionIndex(self.path)
+        return self.__positions
+
+    def part(self, n):
+        return Part(self.path, self._positions.offsets[n], self._positions.lengths[n])
+
+    def number_of_parts(self):
+        return len(self._positions)
+
+
+class BUFRReader(BUFRInOneFile, Reader):
+    """Represents a BUFR file"""
+
+    appendable = True  # BUFR messages can be added to the same file
+
+    def __init__(self, source, path):
+        Reader.__init__(self, source, path)
+        BUFRInOneFile.__init__(self, path)
+
+    def __repr__(self):
+        return "BUFRReader(%s)" % (self.path,)
+
+    @classmethod
+    def merge(cls, readers):
+        assert all(isinstance(s, BUFRReader) for s in readers), readers
+        assert len(readers) > 1
+
+        return MultiBUFRList(readers)
+
+    def mutate_source(self):
+        # A BUFRReader is a source itself
+        return self
diff --git a/earthkit/data/readers/bufr/pandas.py b/earthkit/data/readers/bufr/pandas.py
new file mode 100644
index 0000000..f442dcc
--- /dev/null
+++ b/earthkit/data/readers/bufr/pandas.py
@@ -0,0 +1,70 @@
+# (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 logging
+
+LOG = logging.getLogger(__name__)
+
+COLUMNS = ("latitude", "longitude", "data_datetime")
+
+# todo: remove it when pdbufr with message list support is released
+_MESSAGE_LIST_SUPPORT = None
+
+
+def has_message_list_support():
+    global _MESSAGE_LIST_SUPPORT
+    if _MESSAGE_LIST_SUPPORT is None:
+        try:
+            from pdbufr.bufr_read import _read_bufr  # noqa
+
+            _MESSAGE_LIST_SUPPORT = True
+        except Exception:
+            _MESSAGE_LIST_SUPPORT = False
+
+    return _MESSAGE_LIST_SUPPORT
+
+
+class PandasMixIn:
+    def to_pandas(self, columns=COLUMNS, filters=None, **kwargs):
+        """Extracts BUFR data into a pandas DataFrame using :xref:`pdbufr`.
+
+        Parameters
+        ----------
+        columns: str, sequence[str]
+            List of ecCodes BUFR keys to extract for each BUFR message/subset.
+            See: :xref:`read_bufr` for details.
+        filters: dict
+            Defines the conditions when to extract the specified ``columns``. See:
+            :xref:`read_bufr` for details.
+        **kwargs: dict, optional
+            Other keyword arguments passed to :xref:`read_bufr`.
+
+        Returns
+        -------
+        Pandas DataFrame
+
+        Examples
+        --------
+        :ref:`/examples/bufr_temp.ipynb`
+        :ref:`/examples/bufr_synop.ipynb`
+
+        """
+        import pdbufr
+
+        filters = {} if filters is None else filters
+
+        if has_message_list_support():
+            return pdbufr.read_bufr(self, columns=columns, filters=filters, **kwargs)
+        else:
+            if hasattr(self, "path"):
+                return pdbufr.read_bufr(self.path, columns=columns, filters=filters)
+            else:
+                raise NotImplementedError(
+                    "to_pandas is only supported for single BUFR files"
+                )
diff --git a/earthkit/data/readers/grib/codes.py b/earthkit/data/readers/grib/codes.py
index edbd730..54258e3 100644
--- a/earthkit/data/readers/grib/codes.py
+++ b/earthkit/data/readers/grib/codes.py
@@ -10,14 +10,18 @@
 import datetime
 import logging
 import os
-import threading
-import time
 
 import eccodes
 import numpy as np
 
 from earthkit.data.core import Base
 from earthkit.data.utils.bbox import BoundingBox
+from earthkit.data.utils.message import (
+    CodesHandle,
+    CodesMessagePositionIndex,
+    CodesReader,
+)
+from earthkit.data.utils.metadata import metadata_argument
 from earthkit.data.utils.projections import Projection
 
 LOG = logging.getLogger(__name__)
@@ -32,91 +36,69 @@ def missing_is_none(x):
     return None if x == 2147483647 else x
 
 
-# This does not belong here, should be in the C library
-def get_messages_positions(path):
-    fd = os.open(path, os.O_RDONLY)
-    try:
-
-        def get(count):
-            buf = os.read(fd, count)
-            assert len(buf) == count
-            return int.from_bytes(
-                buf,
-                byteorder="big",
-                signed=False,
-            )
-
-        offset = 0
-        while True:
-            code = os.read(fd, 4)
-            if len(code) < 4:
-                break
-
-            if code != b"GRIB":
-                offset = os.lseek(fd, offset + 1, os.SEEK_SET)
-                continue
-
-            length = get(3)
-            edition = get(1)
+class GribCodesMessagePositionIndex(CodesMessagePositionIndex):
+    # This does not belong here, should be in the C library
+    def _get_message_positions(self, path):
+        fd = os.open(path, os.O_RDONLY)
+        try:
 
-            if edition == 1:
-                if length & 0x800000:
-                    sec1len = get(3)
-                    os.lseek(fd, 4, os.SEEK_CUR)
-                    flags = get(1)
-                    os.lseek(fd, sec1len - 8, os.SEEK_CUR)
+            def get(count):
+                buf = os.read(fd, count)
+                assert len(buf) == count
+                return int.from_bytes(
+                    buf,
+                    byteorder="big",
+                    signed=False,
+                )
 
-                    if flags & (1 << 7):
-                        sec2len = get(3)
-                        os.lseek(fd, sec2len - 3, os.SEEK_CUR)
+            offset = 0
+            while True:
+                code = os.read(fd, 4)
+                if len(code) < 4:
+                    break
 
-                    if flags & (1 << 6):
-                        sec3len = get(3)
-                        os.lseek(fd, sec3len - 3, os.SEEK_CUR)
+                if code != b"GRIB":
+                    offset = os.lseek(fd, offset + 1, os.SEEK_SET)
+                    continue
 
-                    sec4len = get(3)
+                length = get(3)
+                edition = get(1)
 
-                    if sec4len < 120:
-                        length &= 0x7FFFFF
-                        length *= 120
-                        length -= sec4len
-                        length += 4
+                if edition == 1:
+                    if length & 0x800000:
+                        sec1len = get(3)
+                        os.lseek(fd, 4, os.SEEK_CUR)
+                        flags = get(1)
+                        os.lseek(fd, sec1len - 8, os.SEEK_CUR)
 
-            if edition == 2:
-                length = get(8)
+                        if flags & (1 << 7):
+                            sec2len = get(3)
+                            os.lseek(fd, sec2len - 3, os.SEEK_CUR)
 
-            yield offset, length
-            offset = os.lseek(fd, offset + length, os.SEEK_SET)
+                        if flags & (1 << 6):
+                            sec3len = get(3)
+                            os.lseek(fd, sec3len - 3, os.SEEK_CUR)
 
-    finally:
-        os.close(fd)
+                        sec4len = get(3)
 
+                        if sec4len < 120:
+                            length &= 0x7FFFFF
+                            length *= 120
+                            length -= sec4len
+                            length += 4
 
-# For some reason, cffi can ge stuck in the GC if that function
-# needs to be called defined for the first time in a GC thread.
-try:
-    _h = eccodes.codes_new_from_samples(
-        "regular_ll_pl_grib1", eccodes.CODES_PRODUCT_GRIB
-    )
-    eccodes.codes_release(_h)
-except Exception:
-    pass
+                if edition == 2:
+                    length = get(8)
 
+                yield offset, length
+                offset = os.lseek(fd, offset + length, os.SEEK_SET)
 
-class CodesHandle(eccodes.Message):
-    MISSING_VALUE = np.finfo(np.float32).max
-    KEY_TYPES = {"s": str, "l": int, "d": float}
+        finally:
+            os.close(fd)
 
-    def __init__(self, handle, path, offset):
-        super().__init__(handle)
-        self.path = path
-        self.offset = offset
 
-    @classmethod
-    def from_sample(cls, name):
-        return cls(
-            eccodes.codes_new_from_samples(name, eccodes.CODES_PRODUCT_GRIB), None, None
-        )
+class GribCodesHandle(CodesHandle):
+    PRODUCT_ID = eccodes.CODES_PRODUCT_GRIB
 
     # TODO: just a wrapper around the base class implementation to handle the
     # s,l,d qualifiers. Once these are implemented in the base class this method can
@@ -127,16 +109,7 @@ class CodesHandle(eccodes.Message):
         elif name == "md5GridSection":
             return self.get_md5GridSection()
 
-        if ktype is None:
-            name, _, key_type_str = name.partition(":")
-            if key_type_str in CodesHandle.KEY_TYPES:
-                ktype = CodesHandle.KEY_TYPES[key_type_str]
-
-        if "default" in kwargs:
-            return super().get(name, ktype=ktype, **kwargs)
-        else:
-            # this will throw if name is not available
-            return super()._get(name, ktype=ktype)
+        return super().get(name, ktype, **kwargs)
 
     def get_md5GridSection(self):
         # Special case because:
@@ -165,12 +138,6 @@ class CodesHandle(eccodes.Message):
         eccodes.codes_set_long(self._handle, "shapeOfTheEarth", save)
         return result
 
-    def get_string(self, name):
-        return self.get(name, ktype=str)
-
-    def get_long(self, name):
-        return self.get(name, ktype=int)
-
     def as_namespace(self, namespace, param="shortName"):
         r = {}
         ignore = {
@@ -205,8 +172,8 @@ class CodesHandle(eccodes.Message):
     def get_data_points(self):
         return eccodes.codes_grib_get_data(self._handle)
 
-    def clone(self):
-        return CodesHandle(eccodes.codes_clone(self._handle), None, None)
+    # def clone(self):
+    #     return CodesHandle(eccodes.codes_clone(self._handle), None, None)
 
     def set_values(self, values):
         assert self.path is None, "Only cloned handles can have values changed"
@@ -218,118 +185,10 @@ class CodesHandle(eccodes.Message):
         assert self.path is None, "Only cloned handles can have values changed"
         eccodes.codes_set_key_vals(self._handle, values)
 
-    def set_long(self, name, value):
-        try:
-            assert self.path is None, "Only cloned handles can have values changed"
-            eccodes.codes_set_long(self._handle, name, value)
-        except Exception as e:
-            LOG.error("Error setting %s=%s", name, value)
-            LOG.exception(e)
-
-    def set_double(self, name, value):
-        try:
-            assert self.path is None, "Only cloned handles can have values changed"
-            eccodes.codes_set_double(self._handle, name, value)
-        except Exception as e:
-            LOG.error("Error setting %s=%s", name, value)
-            LOG.exception(e)
-
-    def set_string(self, name, value):
-        try:
-            assert self.path is None, "Only cloned handles can have values changed"
-            eccodes.codes_set_string(self._handle, name, value)
-        except Exception as e:
-            LOG.error("Error setting %s=%s", name, value)
-            LOG.exception(e)
-
-    def set(self, name, value):
-        try:
-            assert self.path is None, "Only cloned handles can have values changed"
-
-            if isinstance(value, list):
-                return eccodes.codes_set_array(self._handle, name, value)
-
-            return eccodes.codes_set(self._handle, name, value)
-        except Exception as e:
-            LOG.error("Error setting %s=%s", name, value)
-            LOG.exception(e)
 
-    def write(self, f):
-        eccodes.codes_write(self._handle, f)
-
-    def save(self, path):
-        with open(path, "wb") as f:
-            self.write_to(f)
-            self.path = path
-            self.offset = 0
-
-    def read_bytes(self, offset, length):
-        if self.path is not None:
-            with open(self.path, "rb") as f:
-                f.seek(offset)
-                return f.read(length)
-
-
-class ReaderLRUCache(dict):
-    def __init__(self, size):
-        self.readers = dict()
-        self.lock = threading.Lock()
-        self.size = size
-
-    def __getitem__(self, path):
-        key = (
-            path,
-            os.getpid(),
-        )
-        with self.lock:
-            try:
-                return super().__getitem__(key)
-            except KeyError:
-                pass
-
-            c = self[key] = CodesReader(path)
-            while len(self) >= self.size:
-                _, oldest = min((v.last, k) for k, v in self.items())
-                del self[oldest]
-
-            return c
-
-
-cache = ReaderLRUCache(32)  # TODO: Add to config
-
-
-class CodesReader:
-    def __init__(self, path):
-        self.path = path
-        self.lock = threading.Lock()
-        # print("OPEN", self.path)
-        self.file = open(self.path, "rb")
-        self.last = time.time()
-
-    def __del__(self):
-        try:
-            # print("CLOSE", self.path)
-            self.file.close()
-        except Exception:
-            pass
-
-    @classmethod
-    def from_cache(cls, path):
-        return cache[path]
-
-    def at_offset(self, offset):
-        with self.lock:
-            self.last = time.time()
-            self.file.seek(offset, 0)
-            handle = eccodes.codes_new_from_file(
-                self.file,
-                eccodes.CODES_PRODUCT_GRIB,
-            )
-            assert handle is not None
-            return CodesHandle(handle, self.path, offset)
-
-    def __repr__(self):
-        return f"{self.__class__.__name__}({self.path}"
+class GribCodesReader(CodesReader):
+    PRODUCT_ID = eccodes.CODES_PRODUCT_GRIB
+    HANDLE_TYPE = GribCodesHandle
 
 
 class GribField(Base):
@@ -356,7 +215,7 @@ class GribField(Base):
         r""":class:`CodesHandle`: Gets an object providing access to the low level GRIB message structure."""
         if self._handle is None:
             assert self._offset is not None
-            self._handle = CodesReader.from_cache(self.path).at_offset(self._offset)
+            self._handle = GribCodesReader.from_cache(self.path).at_offset(self._offset)
         return self._handle
 
     @property
@@ -584,52 +443,6 @@ class GribField(Base):
             name = "paramId"
         return self.handle.get(name)
 
-    # TODO: move it into core or util
-    @staticmethod
-    def _parse_metadata_args(*args, namespace=None, astype=None):
-        key = []
-        key_arg_type = None
-        if len(args) == 1 and isinstance(args[0], str):
-            key_arg_type = str
-        elif len(args) >= 1:
-            key_arg_type = tuple
-            for k in args:
-                if isinstance(k, list):
-                    key_arg_type = list
-
-        for k in args:
-            if isinstance(k, str):
-                key.append(k)
-            elif isinstance(k, (list, tuple)):
-                key.extend(k)
-            else:
-                raise ValueError(f"metadata: invalid key argument={k}")
-
-        if key:
-            if namespace is not None:
-                if not isinstance(namespace, str):
-                    raise ValueError(
-                        f"metadata: namespace={namespace} must be a str when key specified"
-                    )
-
-            if isinstance(astype, (list, tuple)):
-                if len(astype) != len(key):
-                    if len(astype) == 1:
-                        astype = [astype[0]] * len(key)
-                    else:
-                        raise ValueError(
-                            "metadata: astype must have the same number of items as key"
-                        )
-            else:
-                astype = [astype] * len(key)
-
-        if namespace is None:
-            namespace = []
-        elif isinstance(namespace, str):
-            namespace = [namespace]
-
-        return (key, namespace, astype, key_arg_type)
-
     def metadata(self, *keys, namespace=None, astype=None, **kwargs):
         r"""Returns metadata values from the GRIB message.
 
@@ -663,7 +476,7 @@ class GribField(Base):
         single value, :obj:`list`, :obj:`tuple` or :obj:`dict`
             - when ``keys`` is not empty:
                 - single value when ``keys`` is a str
-                - otherwise the same type as that of ``keys`` (:obj:`lits` or :obj:`tuple`)
+                - otherwise the same type as that of ``keys`` (:obj:`list` or :obj:`tuple`)
             - when ``keys`` is empty:
                 - when ``namespace`` is :obj:`str` returns a :obj:`dict` with the keys and values
                   in that namespace
@@ -726,7 +539,7 @@ class GribField(Base):
                 key = "paramId"
             return key
 
-        key, namespace, astype, key_arg_type = self._parse_metadata_args(
+        key, namespace, astype, key_arg_type = metadata_argument(
             *keys, namespace=namespace, astype=astype
         )
 
diff --git a/earthkit/data/readers/grib/index/file.py b/earthkit/data/readers/grib/index/file.py
index e57ab59..b6b6f9d 100644
--- a/earthkit/data/readers/grib/index/file.py
+++ b/earthkit/data/readers/grib/index/file.py
@@ -8,12 +8,10 @@
 #
 
 
-import json
 import logging
 import os
 
-from earthkit.data.core.caching import auxiliary_cache_file
-from earthkit.data.readers.grib.codes import get_messages_positions
+from earthkit.data.readers.grib.codes import GribCodesMessagePositionIndex
 from earthkit.data.readers.grib.index import FieldListInFiles
 from earthkit.data.utils.parts import Part
 
@@ -31,65 +29,17 @@ class FieldListInOneFile(FieldListInFiles):
         assert isinstance(path, str), path
 
         self.path = path
-        self.offsets = None
-        self.lengths = None
-        self.mappings_cache_file = auxiliary_cache_file(
-            "grib-index",
-            path,
-            content="null",
-            extension=".json",
-        )
-
-        if not self._load_cache():
-            self._build_offsets_lengths_mapping()
-
+        self.__positions = None
         super().__init__(**kwargs)
 
-    def _build_offsets_lengths_mapping(self):
-        offsets = []
-        lengths = []
-
-        for offset, length in get_messages_positions(self.path):
-            offsets.append(offset)
-            lengths.append(length)
-
-        self.offsets = offsets
-        self.lengths = lengths
-
-        self._save_cache()
-
-    def _save_cache(self):
-        try:
-            with open(self.mappings_cache_file, "w") as f:
-                json.dump(
-                    dict(
-                        version=self.VERSION,
-                        offsets=self.offsets,
-                        lengths=self.lengths,
-                    ),
-                    f,
-                )
-        except Exception:
-            LOG.exception("Write to cache failed %s", self.mappings_cache_file)
-
-    def _load_cache(self):
-        try:
-            with open(self.mappings_cache_file) as f:
-                c = json.load(f)
-                if not isinstance(c, dict):
-                    return False
-
-                assert c["version"] == self.VERSION
-                self.offsets = c["offsets"]
-                self.lengths = c["lengths"]
-                return True
-        except Exception:
-            LOG.exception("Load from cache failed %s", self.mappings_cache_file)
-
-        return False
+    @property
+    def _positions(self):
+        if self.__positions is None:
+            self.__positions = GribCodesMessagePositionIndex(self.path)
+        return self.__positions
 
     def part(self, n):
-        return Part(self.path, self.offsets[n], self.lengths[n])
+        return Part(self.path, self._positions.offsets[n], self._positions.lengths[n])
 
     def number_of_parts(self):
-        return len(self.offsets)
+        return len(self._positions.offsets)
diff --git a/earthkit/data/readers/grib/memory.py b/earthkit/data/readers/grib/memory.py
index e092708..fcab716 100644
--- a/earthkit/data/readers/grib/memory.py
+++ b/earthkit/data/readers/grib/memory.py
@@ -12,7 +12,7 @@ import logging
 import eccodes
 
 from earthkit.data.readers import Reader
-from earthkit.data.readers.grib.codes import CodesHandle, GribField
+from earthkit.data.readers.grib.codes import GribCodesHandle, GribField
 from earthkit.data.readers.grib.index import FieldList
 
 LOG = logging.getLogger(__name__)
@@ -41,7 +41,7 @@ class GribMemoryReader(Reader):
 
     def _message_from_handle(self, handle):
         if handle is not None:
-            return GribFieldInMemory(CodesHandle(handle, None, None))
+            return GribFieldInMemory(GribCodesHandle(handle, None, None))
 
     def peek(self):
         """Returns the next available message without consuming it"""
diff --git a/earthkit/data/readers/grib/output.py b/earthkit/data/readers/grib/output.py
index f3a006b..2bfb99f 100644
--- a/earthkit/data/readers/grib/output.py
+++ b/earthkit/data/readers/grib/output.py
@@ -176,7 +176,7 @@ class GribOutput:
                 metadata["shortName"] = param
 
     def handle_from_metadata(self, values, metadata, compulsory):
-        from .codes import CodesHandle  # Lazy loading of eccodes
+        from .codes import GribCodesHandle  # Lazy loading of eccodes
 
         if len(values.shape) == 1:
             sample = self._gg_field(values, metadata)
@@ -227,8 +227,8 @@ class GribOutput:
                 choices = list_to_human([f"'{c}'" for c in check], "or")
                 raise ValueError(f"Please provide a value for {choices}.")
 
-        LOG.debug("CodesHandle.from_sample(%s)", sample)
-        return CodesHandle.from_sample(sample)
+        LOG.debug("GribCodesHandle.from_sample(%s)", sample)
+        return GribCodesHandle.from_sample(sample)
 
     def _ll_field(self, values, metadata):
         Nj, Ni = values.shape
diff --git a/earthkit/data/readers/grib/parsing.py b/earthkit/data/readers/grib/parsing.py
index 52141dd..4ee95bc 100644
--- a/earthkit/data/readers/grib/parsing.py
+++ b/earthkit/data/readers/grib/parsing.py
@@ -66,7 +66,7 @@ def _index_grib_file(
 ):
     import eccodes
 
-    from earthkit.data.readers.grib.codes import CodesHandle
+    from earthkit.data.readers.grib.codes import GribCodesHandle
 
     post_process_mars = []
     if with_valid_date:
@@ -112,7 +112,7 @@ def _index_grib_file(
         h = eccodes.codes_grib_new_from_file(f)
 
         while h:
-            yield parse_field(CodesHandle(h, path, offset=old_position))
+            yield parse_field(GribCodesHandle(h, path, offset=old_position))
 
             position = f.tell()
             pbar.update(position - old_position)
diff --git a/earthkit/data/utils/message.py b/earthkit/data/utils/message.py
new file mode 100644
index 0000000..ac1c70a
--- /dev/null
+++ b/earthkit/data/utils/message.py
@@ -0,0 +1,264 @@
+# (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 json
+import logging
+import os
+import threading
+import time
+
+import eccodes
+import numpy as np
+
+from earthkit.data.core.caching import auxiliary_cache_file
+
+LOG = logging.getLogger(__name__)
+
+# For some reason, cffi can get stuck in the GC if that function
+# needs to be called defined for the first time in a GC thread.
+try:
+    _h = eccodes.codes_new_from_samples(
+        "regular_ll_pl_grib1", eccodes.CODES_PRODUCT_GRIB
+    )
+    eccodes.codes_release(_h)
+except Exception:
+    pass
+
+
+class CodesMessagePositionIndex:
+    VERSION = 1
+
+    def __init__(self, path):
+        self.path = path
+        self.offsets = None
+        self.lengths = None
+        self._cache_file = None
+        self._load()
+
+    def __len__(self):
+        return len(self.offsets)
+
+    def _get_message_positions(self, path):
+        raise NotImplementedError
+
+    def _build(self):
+        offsets = []
+        lengths = []
+
+        for offset, length in self._get_message_positions(self.path):
+            offsets.append(offset)
+            lengths.append(length)
+
+        self.offsets = offsets
+        self.lengths = lengths
+
+    def _load(self):
+        if True:
+            # if SETTINGS.policy("message-position-cache"):
+            self._cache_file = auxiliary_cache_file(
+                "message-index",
+                self.path,
+                content="null",
+                extension=".json",
+            )
+            if not self._load_cache():
+                self._build()
+                self._save_cache()
+        else:
+            self._build()
+
+    def _save_cache(self):
+        # assert SETTINGS.policy("message-position-cache")
+        try:
+            with open(self._cache_file, "w") as f:
+                json.dump(
+                    dict(
+                        version=self.VERSION,
+                        offsets=self.offsets,
+                        lengths=self.lengths,
+                    ),
+                    f,
+                )
+        except Exception:
+            LOG.exception("Write to cache failed %s", self._cache_file)
+
+    def _load_cache(self):
+        # assert SETTINGS.policy("message-position-cache")
+        try:
+            with open(self._cache_file) as f:
+                c = json.load(f)
+                if not isinstance(c, dict):
+                    return False
+
+                assert c["version"] == self.VERSION
+                self.offsets = c["offsets"]
+                self.lengths = c["lengths"]
+                return True
+        except Exception:
+            LOG.exception("Load from cache failed %s", self._cache_file)
+
+        return False
+
+
+class CodesHandle(eccodes.Message):
+    MISSING_VALUE = np.finfo(np.float32).max
+    KEY_TYPES = {"s": str, "l": int, "d": float}
+    PRODUCT_ID = None
+
+    def __init__(self, handle, path, offset):
+        super().__init__(handle)
+        self.path = path
+        self.offset = offset
+
+    @classmethod
+    def from_sample(cls, name):
+        return cls(eccodes.codes_new_from_samples(name, cls.PRODUCT_ID), None, None)
+
+    # TODO: just a wrapper around the base class implementation to handle the
+    # s,l,d qualifiers. Once these are implemented in the base class this method can
+    # be removed. md5GridSection is also handled!
+    def get(self, name, ktype=None, **kwargs):
+        if ktype is None:
+            name, _, key_type_str = name.partition(":")
+            if key_type_str in CodesHandle.KEY_TYPES:
+                ktype = CodesHandle.KEY_TYPES[key_type_str]
+
+        if "default" in kwargs:
+            return super().get(name, ktype=ktype, **kwargs)
+        else:
+            # this will throw if name is not available
+            return super()._get(name, ktype=ktype)
+
+    def get_string(self, name):
+        return self.get(name, ktype=str)
+
+    def get_long(self, name):
+        return self.get(name, ktype=int)
+
+    def clone(self):
+        return CodesHandle(eccodes.codes_clone(self._handle), None, None)
+
+    def set_multiple(self, values):
+        assert self.path is None, "Only cloned handles can have values changed"
+        eccodes.codes_set_key_vals(self._handle, values)
+
+    def set_long(self, name, value):
+        try:
+            assert self.path is None, "Only cloned handles can have values changed"
+            eccodes.codes_set_long(self._handle, name, value)
+        except Exception as e:
+            LOG.error("Error setting %s=%s", name, value)
+            LOG.exception(e)
+
+    def set_double(self, name, value):
+        try:
+            assert self.path is None, "Only cloned handles can have values changed"
+            eccodes.codes_set_double(self._handle, name, value)
+        except Exception as e:
+            LOG.error("Error setting %s=%s", name, value)
+            LOG.exception(e)
+
+    def set_string(self, name, value):
+        try:
+            assert self.path is None, "Only cloned handles can have values changed"
+            eccodes.codes_set_string(self._handle, name, value)
+        except Exception as e:
+            LOG.error("Error setting %s=%s", name, value)
+            LOG.exception(e)
+
+    def set(self, name, value):
+        try:
+            assert self.path is None, "Only cloned handles can have values changed"
+
+            if isinstance(value, list):
+                return eccodes.codes_set_array(self._handle, name, value)
+
+            return eccodes.codes_set(self._handle, name, value)
+        except Exception as e:
+            LOG.error("Error setting %s=%s", name, value)
+            LOG.exception(e)
+
+    def write(self, f):
+        eccodes.codes_write(self._handle, f)
+
+    def save(self, path):
+        with open(path, "wb") as f:
+            self.write_to(f)
+            self.path = path
+            self.offset = 0
+
+    def read_bytes(self, offset, length):
+        if self.path is not None:
+            with open(self.path, "rb") as f:
+                f.seek(offset)
+                return f.read(length)
+
+
+class ReaderLRUCache(dict):
+    def __init__(self, size):
+        self.readers = dict()
+        self.lock = threading.Lock()
+        self.size = size
+
+    def __getitem__(self, path_and_cls):
+        path = path_and_cls[0]
+        cls = path_and_cls[1]
+        key = (path, os.getpid())
+        with self.lock:
+            try:
+                return super().__getitem__(key)
+            except KeyError:
+                pass
+
+            c = self[key] = cls(path)
+            while len(self) >= self.size:
+                _, oldest = min((v.last, k) for k, v in self.items())
+                del self[oldest]
+
+            return c
+
+
+cache = ReaderLRUCache(32)  # TODO: Add to config
+
+
+class CodesReader:
+    PRODUCT_ID = None
+    HANDLE_TYPE = None
+
+    def __init__(self, path):
+        self.path = path
+        self.lock = threading.Lock()
+        # print("OPEN", self.path)
+        self.file = open(self.path, "rb")
+        self.last = time.time()
+
+    def __del__(self):
+        try:
+            # print("CLOSE", self.path)
+            self.file.close()
+        except Exception:
+            pass
+
+    @classmethod
+    def from_cache(cls, path):
+        return cache[(path, cls)]
+
+    def at_offset(self, offset):
+        with self.lock:
+            self.last = time.time()
+            self.file.seek(offset, 0)
+            handle = eccodes.codes_new_from_file(
+                self.file,
+                self.PRODUCT_ID,
+            )
+            assert handle is not None
+            return self.HANDLE_TYPE(handle, self.path, offset)
+
+    def __repr__(self):
+        return f"{self.__class__.__name__}({self.path}"
diff --git a/earthkit/data/utils/metadata.py b/earthkit/data/utils/metadata.py
new file mode 100644
index 0000000..3c6b58d
--- /dev/null
+++ b/earthkit/data/utils/metadata.py
@@ -0,0 +1,54 @@
+# (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.
+#
+
+
+def metadata_argument(*args, namespace=None, astype=None):
+    """Helps parsing the input arguments for the metadata methods"""
+    key = []
+    key_arg_type = None
+    if len(args) == 1 and isinstance(args[0], str):
+        key_arg_type = str
+    elif len(args) >= 1:
+        key_arg_type = tuple
+        for k in args:
+            if isinstance(k, list):
+                key_arg_type = list
+
+    for k in args:
+        if isinstance(k, str):
+            key.append(k)
+        elif isinstance(k, (list, tuple)):
+            key.extend(k)
+        else:
+            raise ValueError(f"metadata: invalid key argument={k}")
+
+    if key:
+        if namespace is not None:
+            if not isinstance(namespace, str):
+                raise ValueError(
+                    f"metadata: namespace={namespace} must be a str when key specified"
+                )
+
+        if isinstance(astype, (list, tuple)):
+            if len(astype) != len(key):
+                if len(astype) == 1:
+                    astype = [astype[0]] * len(key)
+                else:
+                    raise ValueError(
+                        "metadata: astype must have the same number of items as key"
+                    )
+        else:
+            astype = [astype] * len(key)
+
+    if namespace is None:
+        namespace = []
+    elif isinstance(namespace, str):
+        namespace = [namespace]
+
+    return (key, namespace, astype, key_arg_type)
diff --git a/earthkit/data/utils/summary.py b/earthkit/data/utils/summary.py
index 1bdfe79..4e85608 100644
--- a/earthkit/data/utils/summary.py
+++ b/earthkit/data/utils/summary.py
@@ -157,3 +157,190 @@ def format_namespace_dump(data, selected=None, details=None, **kwargs):
         return NamespaceDump(data, selected=selected, details=details)
     else:
         return data
+
+
+class BUFRTree:
+    """Restructures the result of the bufr_dump ecCodes command."""
+
+    def __init__(self, data, subset, compressed, uncompressed):
+        self.data = data
+        self.subset = subset
+        self.compressed = compressed
+        self.uncompressed = uncompressed
+
+    def make_tree(self):
+        """Restructures the the result of json bufr_dump into a format better
+        suited for generating a tree view out of it.
+
+        Returns
+        -------
+        dict
+        """
+        return self._load_dump()
+
+    def _load_dump(self):
+        r = {"header": [], "data": []}
+
+        # data = data["messages"][0]
+
+        for i, v in enumerate(self.data):
+            # print(v)
+            k = v["key"]
+            val = v["value"]
+            if isinstance(val, list):
+                val = "array"
+
+            r["header"].append({"key": k, "value": val})
+
+            # start data section
+            if k == "unexpandedDescriptors":
+                # for uncompressed data tries to shown only the branch
+                # with given subset
+                if self.subset is not None and self.subset > 0 and self.uncompressed:
+                    try:
+                        subset_index = self.subset - 1
+                        d = self.data[i + 1][subset_index][0]
+                        if d["key"] == "subsetNumber" and d["value"] == self.subset:
+                            self._parse_dump(self.data[i + 1][subset_index], r["data"])
+                            break
+                    except Exception:
+                        pass
+                self._parse_dump(self.data[i + 1], r["data"])
+                break
+        return r
+
+    def _parse_dump(self, data, parent):
+        arrayCnt = 0
+        keyCnt = 0
+        for v in data:
+            if isinstance(v, list):
+                arrayCnt += 1
+            else:
+                keyCnt += 1
+
+        for v in data:
+            if not isinstance(v, list):
+                vals = v["value"]
+                if isinstance(vals, list):
+                    vals = self._format_list(vals)
+                parent.append(
+                    {
+                        "key": v["key"],
+                        "value": vals,
+                        "units": v.get("units", None),
+                    }
+                )
+            else:
+                if arrayCnt > 1:
+                    item = []
+                    parent.append(item)
+                    self._parse_dump(v, item)
+                else:
+                    self._parse_dump(v, parent)
+
+    def _format_list(self, vals):
+        if len(vals) > 0:
+            if self.subset is not None and self.subset > 0:
+                index = self.subset - 1
+                if index < len(vals):
+                    return f"{vals[index]} ({len(vals)} items)"
+                else:
+                    return f"[{vals[0]}, ...] ({len(vals)-1} items)"
+            else:
+                return f"[{vals[0]}, ...] ({len(vals)} items)"
+        else:
+            return "[]"
+
+
+class BUFRHtmlTree:
+    def make_html(self, data):
+        """Generates a html/css tree view from the input representing the
+        result of bufr_dump.
+
+        Parameters
+        ----------
+        data: dict
+
+        Returns
+        -------
+        str
+        """
+        return self._build(data)
+
+    def _build(self, data):
+        td1 = self._node(data["header"], [])
+        td2 = self._node(data["data"], [])
+
+        td = self._top_node("header", td1) + self._top_node("data", td2)
+        t = f"""
+        <ul class="tree">
+        <li>
+            {td}
+        </li>
+        </ul>
+        """
+        return t
+
+    def _leaf(self, k, v, units):
+        if units is not None:
+            return f"""<li>{k}: {v} [{units}]</li>"""
+        else:
+            return f"""<li>{k}: {v}</li>"""
+
+    def _top_node(self, name, v):
+        return f"""
+        <li>
+        <details>
+            <summary>{name}</summary>
+            <ul>
+            {v}
+            </ul>
+            </details>
+        </li>
+        """
+
+    def _start_node(self, k, v):
+        return f"""
+        <li>
+        <details>
+            <summary>{k}: {v}</summary>
+            <ul>
+        """
+
+    def _end_node(self):
+        return """</ul>
+            </details>
+        </li>
+        """
+
+    def _node(self, data, parent):
+        td = ""
+        if isinstance(data, list):
+            if parent:
+                td += self._start_node(data[0]["key"], data[0]["value"])
+            for v in data:
+                td += self._node(v, data)
+            if parent:
+                td += self._end_node()
+        else:
+            td += self._leaf(data["key"], data["value"], data.get("units", None))
+
+        return td
+
+
+def make_bufr_html_tree(data, title, subset, compressed, uncompressed):
+    tree = BUFRTree(data, subset, compressed, uncompressed).make_tree()
+    if ipython_active:
+        from IPython.display import HTML
+
+        from earthkit.data.utils.html import css
+
+        t = ""
+        if title is not None and title:
+            t = f"""<div class="eh-description">{title}</div>"""
+
+        t += BUFRHtmlTree().make_html(tree)
+        style = css("tree")
+        return HTML(style + t)
+
+    return tree
diff --git a/environment.yml b/environment.yml
index 31d9a49..a3ed7d6 100644
--- a/environment.yml
+++ b/environment.yml
@@ -12,7 +12,7 @@ dependencies:
 - dask
 - netcdf4
 - cfgrib>=0.9.10.1
-- pdbufr
+- pdbufr>=0.10.3
 - pyodc
 - filelock
 - pyyaml
diff --git a/setup.cfg b/setup.cfg
index d1d3b0c..edd69a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -29,7 +29,7 @@ install_requires =
     jinja2
     multiurl
     netcdf4
-    pdbufr
+    pdbufr>=0.10.3
     pyfdb
     pyodc
     pyyaml


Test patch:
diff --git a/tests/bufr/test_bufr_concat.py b/tests/bufr/test_bufr_concat.py
new file mode 100644
index 0000000..d2b1e74
--- /dev/null
+++ b/tests/bufr/test_bufr_concat.py
@@ -0,0 +1,31 @@
+#!/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.
+#
+
+from earthkit.data import from_source
+from earthkit.data.testing import earthkit_examples_file, earthkit_remote_test_data_file
+
+
+def test_bufr_concat():
+    ds1 = from_source("file", earthkit_examples_file("temp_10.bufr"))
+    ds2 = from_source("url", earthkit_remote_test_data_file("examples/synop_10.bufr"))
+    ds = ds1 + ds2
+
+    assert len(ds) == 20
+    md = [x._header("dataCategory") for x in ds1] + [
+        x._header("dataCategory") for x in ds2
+    ]
+    assert md == [2] * 10 + [0] * 10
+
+
+if __name__ == "__main__":
+    from earthkit.data.testing import main
+
+    main()
diff --git a/tests/bufr/test_bufr_contents.py b/tests/bufr/test_bufr_contents.py
new file mode 100644
index 0000000..e6f5519
--- /dev/null
+++ b/tests/bufr/test_bufr_contents.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+# (C) Copyright 2022- 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.
+
+from earthkit.data import from_source
+from earthkit.data.testing import earthkit_examples_file, earthkit_remote_test_data_file
+
+
+def test_bufr_iteration():
+    ds = from_source("file", earthkit_examples_file("temp_10.bufr"))
+    assert len(ds) == 10
+    assert len([x for x in ds]) == 10
+    for f in ds:
+        assert f.subset_count() == 1
+
+
+def test_bufr_message_repr():
+    ds = from_source("file", earthkit_examples_file("temp_10.bufr"))
+    assert "BUFRMessage" in ds[0].__repr__()
+
+
+def test_bufr_metadata():
+    ds = from_source("file", earthkit_examples_file("temp_10.bufr"))
+    assert ds[0].subset_count() == 1
+    assert ds[0].is_compressed() is False
+    assert ds[0].is_uncompressed() is False
+
+
+def test_bufr_metadata_uncompressed():
+    ds = from_source(
+        "url",
+        earthkit_remote_test_data_file(
+            "test-data/synop_multi_subset_uncompressed.bufr"
+        ),
+    )
+    assert len(ds) == 1
+    f = ds[0]
+    assert f.subset_count() == 12
+    assert f.is_compressed() is False
+    assert f.is_uncompressed() is True
+
+
+def test_bufr_metadata_compressed():
+    ds = from_source(
+        "url",
+        earthkit_remote_test_data_file("test-data/ens_multi_subset_compressed.bufr"),
+    )
+    assert len(ds) == 1
+    f = ds[0]
+    assert f.subset_count() == 51
+    assert f.is_compressed() is True
+    assert f.is_uncompressed() is False
+
+
+if __name__ == "__main__":
+    from earthkit.data.testing import main
+
+    main()
diff --git a/tests/bufr/test_bufr_convert.py b/tests/bufr/test_bufr_convert.py
index b9c9925..59c5611 100644
--- a/tests/bufr/test_bufr_convert.py
+++ b/tests/bufr/test_bufr_convert.py
@@ -13,6 +13,7 @@ import pandas as pd
 import pytest
 
 from earthkit.data import from_source
+from earthkit.data.readers.bufr.pandas import has_message_list_support
 from earthkit.data.testing import earthkit_examples_file
 
 assert_frame_equal = pd.testing.assert_frame_equal
@@ -54,6 +55,20 @@ def test_bufr_to_pandas_filters():
     assert_frame_equal(res, ref[res.columns])
 
 
+@pytest.mark.skipif(not has_message_list_support(), reason="not implemented")
+def test_bufr_to_pandas_multi():
+    print(f"support={has_message_list_support()}")
+    ds = from_source("file", earthkit_examples_file("temp_10.bufr")) + from_source(
+        "file", earthkit_examples_file("synop_10.bufr")
+    )
+
+    res = ds.to_pandas(
+        columns=["latitude", "longitude", "WMO_station_id"],
+    )
+
+    assert len(res) == 20
+
+
 if __name__ == "__main__":
     from earthkit.data.testing import main
 
diff --git a/tests/bufr/test_bufr_summary.py b/tests/bufr/test_bufr_summary.py
index f9b063b..f9b82db 100644
--- a/tests/bufr/test_bufr_summary.py
+++ b/tests/bufr/test_bufr_summary.py
@@ -11,7 +11,7 @@
 import pytest
 
 from earthkit.data import from_source
-from earthkit.data.testing import earthkit_examples_file
+from earthkit.data.testing import earthkit_examples_file, earthkit_remote_test_data_file
 
 
 def test_bufr_ls_invalid_num():
@@ -124,3 +124,85 @@ def test_bufr_tail_num():
 
     df = f.tail(2)
     assert ref == df.to_dict()
+
+
+def test_bufr_dump():
+    ds = from_source("file", earthkit_examples_file("temp_10.bufr"))
+    r = ds[0].dump()
+    assert isinstance(r, dict)
+    assert "header" in r
+    assert "data" in r
+
+
+@pytest.mark.parametrize(
+    "_kwargs,expected_val", [({"subset": 1}, 1), ({}, 1), ({"subset": 2}, 2)]
+)
+def test_bufr_dump_uncompressed(_kwargs, expected_val):
+    ds = from_source(
+        "url",
+        earthkit_remote_test_data_file(
+            "test-data/synop_multi_subset_uncompressed.bufr"
+        ),
+    )
+    n = ds[0].subset_count()
+    assert n == 12
+
+    r = ds[0].dump(**_kwargs)
+    assert isinstance(r, dict)
+    assert "header" in r
+    assert "data" in r
+    assert r["data"][0] == dict(key="subsetNumber", value=expected_val, units=None)
+
+
+@pytest.mark.parametrize("_kwargs", [{"subset": 0}, {"subset": None}])
+def test_bufr_dump_uncompressed_full(_kwargs):
+    ds = from_source(
+        "url",
+        earthkit_remote_test_data_file(
+            "test-data/synop_multi_subset_uncompressed.bufr"
+        ),
+    )
+
+    n = ds[0].subset_count()
+    assert n == 12
+
+    r = ds[0].dump(**_kwargs)
+    assert isinstance(r, dict)
+    assert "header" in r
+    assert "data" in r
+    d = r["data"]
+    for i in range(n):
+        assert d[i][0] == dict(key="subsetNumber", value=i + 1, units=None)
+
+
+@pytest.mark.parametrize(
+    "_kwargs, expected_val",
+    [
+        ({"subset": 0}, "[0, ...] (51 items)"),
+        ({"subset": None}, "[0, ...] (51 items)"),
+        ({"subset": 1}, "0 (51 items)"),
+        ({}, "0 (51 items)"),
+        ({"subset": 2}, "1 (51 items)"),
+    ],
+)
+def test_bufr_dump_compressed(_kwargs, expected_val):
+    ds = from_source(
+        "url",
+        earthkit_remote_test_data_file("test-data/ens_multi_subset_compressed.bufr"),
+    )
+    n = ds[0].subset_count()
+    assert n == 51
+
+    r = ds[0].dump(**_kwargs)
+    assert isinstance(r, dict)
+    assert "header" in r
+    assert "data" in r
+    assert r["data"][1] == dict(
+        key="ensembleMemberNumber", value=expected_val, units="Numeric"
+    )
+
+
+if __name__ == "__main__":
+    from earthkit.data.testing import main
+
+    main()
diff --git a/tests/environment-unit-tests.yml b/tests/environment-unit-tests.yml
index 1c36501..bad4df6 100644
--- a/tests/environment-unit-tests.yml
+++ b/tests/environment-unit-tests.yml
@@ -13,7 +13,7 @@ dependencies:
 - dask
 - netcdf4
 - cfgrib>=0.9.10.1
-- pdbufr
+- pdbufr>=0.10.3
 - pyodc
 - filelock
 - pyyaml