Repository: ecmwf/earthkit-data (python)

Issue:
Enable controlling settings via environment variables
### Is your feature request related to a problem? Please describe.

Currently, the settings can only be controlled via the `~/.earthkit-data/settings.yaml` file or from Python using the `earthkit.data.settings` interface. Additionally, it would be useful (e.g. in ecflow)  if we were able to control the settings via env variables too.

It is not clear whether we should add an env variable for each settings or only for a subset of these. The proposal here is to  only add env variables for the settings controlling the cache and the temporary directory:

- cache-policy
- temporary-cache-directory-root
- temporary-directory-root
- maximum-cache-size
- maximum-cache-disk-usage

The env variables should be all upper-case prefixed with EARTHKIT_DATA. E.g.:

cache-policy -> EARTHKIT_DATA_CACHE_POLICY

### Describe the solution you'd like

_No response_

### Describe alternatives you've considered

_No response_

### Additional context

_No response_

### Organisation

ECMWF

Pull request:
Enable using environment variables for settings
Fixes #561 

Patch:
diff --git a/docs/examples/index.rst b/docs/examples/index.rst
index 0deca5e..bb31dc1 100644
--- a/docs/examples/index.rst
+++ b/docs/examples/index.rst
@@ -172,6 +172,7 @@ Miscellaneous
     :glob:
 
     settings.ipynb
+    settings_env_vars.ipynb
     cache.ipynb
     projection.ipynb
     metadata.ipynb
diff --git a/docs/examples/settings.ipynb b/docs/examples/settings.ipynb
index 2dac3c5..0d8f993 100644
--- a/docs/examples/settings.ipynb
+++ b/docs/examples/settings.ipynb
@@ -72,7 +72,13 @@
    "cell_type": "code",
    "execution_count": 2,
    "id": "c5d47016-6f5b-4cbc-ac15-1d231546ac10",
-   "metadata": {},
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
    "outputs": [],
    "source": [
     "settings.auto_save_settings = False"
diff --git a/docs/examples/settings_env_vars.ipynb b/docs/examples/settings_env_vars.ipynb
new file mode 100644
index 0000000..a78ac01
--- /dev/null
+++ b/docs/examples/settings_env_vars.ipynb
@@ -0,0 +1,386 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "1273fa5f-9ab5-4a4c-857a-0f212e759f12",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "## Settings environment variables"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "75bffc4a-24e8-4d2d-948d-2cb84bc11020",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "from earthkit.data import settings"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "022b7ed2-29f2-49fb-a619-ff86f45d7a48",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "For the rest of this notebook we disable the :ref:`settings`  autosave so the changes will not be written into our configuration file."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "2072ae67-13df-4e8a-a1a4-d54a56639569",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "settings.auto_save_settings = False"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "0c497859-6d2a-4fb7-bfb0-0979eb0d2279",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "Each :ref:`settings` parameter has a corresponding environment variable (see the full list :ref:`here <settings_env_table>`). When an environment variable is set, it takes precedence over the settings parameter as the following example demonstrates it."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2888d0b6-beea-4178-9c01-5a08a5be7ad2",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "Assuming no environmental variable is set the value is read form the settings file."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "21f853d3-c961-4ad2-b200-c24f7b03a54e",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "5"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "settings.get(\"number-of-download-threads\")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "95036166-0eec-437b-9988-771e522ada2e",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "When the environment variable is set :func:`get <settings_get>` returns its value."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "d4a8c3bd-19ca-454a-8f39-07de7a2d318c",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "env: EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS=26\n"
+     ]
+    }
+   ],
+   "source": [
+    "%env EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS=26"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "dbc54f27-7b5e-467b-bcba-28daea0e3c46",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "26"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "settings.get(\"number-of-download-threads\")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "4d784930-1a1e-482c-ae2d-7491a4066b47",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "Setting the value generates a warning. The new value is saved into the  settings file, but :func:`get <settings_get>` still returns the value of the environment variable."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "8ee6f835-ae42-4eae-bc96-a53c47f3ce06",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/Users/cgr/git/earthkit-data/src/earthkit/data/core/settings.py:404: UserWarning: Setting 'number-of-download-threads' is also set by environment variable 'EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS'.The environment variable takes precedence and its value is returned when calling get().\n",
+      "  warnings.warn(msg)\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "26"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "settings.set(\"number-of-download-threads\", 10)\n",
+    "settings.get(\"number-of-download-threads\")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "0a24b839-a4d1-4340-bd87-7d690f0511f7",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "The :func:`env` method gives details about the set environment variables."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "9c56e84d-ff1e-4415-b924-b461d62ddbbd",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'number-of-download-threads': ('EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS',\n",
+       "  '26')}"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "settings.env()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d3e74276-cf79-42f0-9b66-b59ac15829d3",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "When we dump the settings the values set via environment variables are clearly indicated."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "89a92e24-f7f5-4f04-bc9a-1d0d00f7c8cf",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<style type=\"text/css\">table.ek td {\n",
+       "    vertical-align: top;\n",
+       "    text-align: left !important;\n",
+       "}\n",
+       "\n",
+       "table.ek th {\n",
+       "    vertical-align: top;\n",
+       "    text-align: left !important;\n",
+       "}\n",
+       "</style><table class='ek'><tr><th>Name</th><th>Value</th><th>Default</th></tr><tr><td>cache-policy</td><td>'off'</td><td>'off'</td></tr><tr><td>check-out-of-date-urls</td><td>True</td><td>True</td></tr><tr><td>download-out-of-date-urls</td><td>False</td><td>False</td></tr><tr><td>grib-field-policy</td><td>'persistent'</td><td>'persistent'</td></tr><tr><td>grib-handle-cache-size</td><td>1</td><td>1</td></tr><tr><td>grib-handle-policy</td><td>'cache'</td><td>'cache'</td></tr><tr><td>maximum-cache-disk-usage</td><td>'95%'</td><td>'95%'</td></tr><tr><td>maximum-cache-size</td><td>None</td><td>None</td></tr><tr><td>number-of-download-threads</td><td>EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS='26'<br>(10)</td><td>5</td></tr><tr><td>reader-type-check-bytes</td><td>64</td><td>64</td></tr><tr><td>temporary-cache-directory-root</td><td>None</td><td>None</td></tr><tr><td>temporary-directory-root</td><td>None</td><td>None</td></tr><tr><td>url-download-timeout</td><td>'30s'</td><td>'30s'</td></tr><tr><td>use-grib-metadata-cache</td><td>True</td><td>True</td></tr><tr><td>use-message-position-index-cache</td><td>False</td><td>False</td></tr><tr><td>use-standalone-mars-client-when-available</td><td>True</td><td>True</td></tr><tr><td>user-cache-directory</td><td>'/var/folders/93/w0p869rx17q98wxk83gn9ys40000gn/T/earthkit-data-cgr'</td><td>'/var/folders/93/w0p869rx17q98wxk83gn9ys40000gn/T/earthkit-data-cgr'</td></tr><tr><td>version</td><td>'0.11.5.dev2+g384bbb0.d20241209'</td><td>''</td></tr></table>"
+      ],
+      "text/plain": [
+       "cache-policy: (off, off)\n",
+       "check-out-of-date-urls: (True, True)\n",
+       "download-out-of-date-urls: (False, False)\n",
+       "grib-field-policy: (persistent, persistent)\n",
+       "grib-handle-cache-size: (1, 1)\n",
+       "grib-handle-policy: (cache, cache)\n",
+       "maximum-cache-disk-usage: (95%, 95%)\n",
+       "maximum-cache-size: (None, None)\n",
+       "number-of-download-threads: (EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS=26, 10, 5)\n",
+       "reader-type-check-bytes: (64, 64)\n",
+       "temporary-cache-directory-root: (None, None)\n",
+       "temporary-directory-root: (None, None)\n",
+       "url-download-timeout: (30s, 30s)\n",
+       "use-grib-metadata-cache: (True, True)\n",
+       "use-message-position-index-cache: (False, False)\n",
+       "use-standalone-mars-client-when-available: (True, True)\n",
+       "user-cache-directory: (/var/folders/93/w0p869rx17q98wxk83gn9ys40000gn/T/earthkit-data-cgr, /var/folders/93/w0p869rx17q98wxk83gn9ys40000gn/T/earthkit-data-cgr)\n",
+       "version: (0.11.5.dev2+g384bbb0.d20241209, )"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "settings"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "943ec49a-b11f-490f-9c58-7b942eaaeaa9",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "dev_ecc",
+   "language": "python",
+   "name": "dev_ecc"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.13"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/guide/settings.rst b/docs/guide/settings.rst
index 3d904de..cc16042 100644
--- a/docs/guide/settings.rst
+++ b/docs/guide/settings.rst
@@ -6,12 +6,14 @@ Settings
 *earthkit-data* is maintaining a set of global settings which control
 its behaviour.
 
-The settings are automatically saved in ``~/.earthkit-data/settings.yaml`` and they can
-be accessed and modified from Python.
+The settings are automatically loaded from and saved into ``~/.earthkit-data/settings.yaml`` and they can
+be accessed and modified from Python. The settings can also be defined as :ref:`environment variables <settings_env>`, which take precedence over the settings file.
 
-.. tip::
+See the following notebooks for examples:
+
+    - :ref:`/examples/settings.ipynb`
+    - :ref:`/examples/settings_env_vars.ipynb`
 
-    See the :ref:`/examples/settings.ipynb` notebook for examples.
 
 .. _settings_get:
 
@@ -22,6 +24,9 @@ earthkit-data settings can be accessed using the python API:
 
 .. literalinclude:: include/settings-get.py
 
+.. warning::
+
+    When an :ref:`environment variable <settings_env>` is set, it takes precedence over the settings parameter, and its value is returned from :func:`get() <settings_get>`.
 
 .. _settings_set:
 
@@ -37,6 +42,11 @@ earthkit-data settings can be modified using the python API:
 
 .. literalinclude:: include/settings-set.py
 
+.. warning::
+
+    When an :ref:`environment variable <settings_env>` is set, the new value provided for :func:`set() <settings_set>` is saved into the settings file but :func:`get() <settings_get>` wil still return the value of the environment variable. A warning is also generated.
+
+
 .. _settings_temporary:
 
 Temporary settings
@@ -52,6 +62,10 @@ Output::
     12
     11
 
+.. warning::
+
+    When an :ref:`environment variable <settings_env>` is set, the same rules applies as for :func:`set() <settings_set>`.
+
 
 .. _settings_reset:
 
@@ -67,12 +81,82 @@ earthkit-data settings can be reset using the python API:
 
 .. literalinclude:: include/settings-reset.py
 
+.. warning::
+
+    When an :ref:`environment variable <settings_env>` is set, the same rules applies as for :func:`set() <settings_set>`.
+
+
+.. _settings_env:
+
+Environment variables
+----------------------
+
+Each settings parameter has a corresponding environment variable (see the full list :ref:`here <settings_env_table>`). When an environment variable is set, it takes precedence over the settings parameter as the following examples show.
+
+First, let us assume that the value of  ``number-of-download-threads`` is 5 in the settings file and no environment variable is set.
+
+.. code-block:: python
+
+    >>> from earthkit.data import settings
+    >>> settings.get("number-of-download-threads")
+    5
+
+Then, set the environment variable ``EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS``.
+
+.. code-block:: bash
+
+    export EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS=26
+
+
+.. code-block:: python
+
+    >>> from earthkit.data import settings
+    >>> settings.get("number-of-download-threads")
+    26
+    >>> settings.env()
+    {'number-of-download-threads': ('EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS', '26')}
+    >>> settings.set("number-of-download-threads", 10)
+    UserWarning: Setting 'number-of-download-threads' is also set by environment variable
+    'EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS'.The environment variable takes precedence and
+    its value is returned when calling get(). Still, the value set here will be
+    saved to the settings file.
+    >>> settings.get("number-of-download-threads")
+    26
+
+Finally, unset the environment variable and check the settings value again, which is now the value from the settings file.
+
+.. code-block:: bash
+
+    unset EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS
+
+
+.. code-block:: python
+
+    >>> from earthkit.data import settings
+    >>> settings.get("number-of-download-threads")
+    10
+
+
+See also the following notebook:
+
+    - :ref:`/examples/settings_env_vars.ipynb`
+
 
 .. _settings_table:
 
-Settings parameters
--------------------
+List of settings parameters
+----------------------------
 
 This is the list of all the settings parameters:
 
 .. module-output:: generate_settings_rst
+
+
+.. _settings_env_table:
+
+List of environment variables
+---------------------------------
+
+This is the list of the settings environment variables:
+
+.. module-output:: generate_settings_env_rst
diff --git a/src/earthkit/data/core/settings.py b/src/earthkit/data/core/settings.py
index 6c5b100..a4ee81b 100644
--- a/src/earthkit/data/core/settings.py
+++ b/src/earthkit/data/core/settings.py
@@ -12,6 +12,7 @@ import getpass
 import logging
 import os
 import tempfile
+import warnings
 from abc import ABCMeta
 from abc import abstractmethod
 from contextlib import contextmanager
@@ -33,6 +34,8 @@ LOG = logging.getLogger(__name__)
 DOT_EARTHKIT_DATA = os.path.expanduser("~/.earthkit_data")
 EARTHKIT_SETTINGS_DIR = DOT_EARTHKIT_DATA
 
+ENV_PREFIX = "EARTHKIT_DATA_"
+
 
 class Validator(metaclass=ABCMeta):
     @abstractmethod
@@ -163,6 +166,7 @@ SETTINGS_AND_HELP = {
     "number-of-download-threads": _(
         5,
         """Number of threads used to download data.""",
+        getter="_as_int",
     ),
     "cache-policy": _(
         "off",
@@ -210,6 +214,7 @@ SETTINGS_AND_HELP = {
         64,
         """Number of bytes read from the beginning of a source to identify its type.
         {validator}""",
+        getter="_as_int",
         validator=IntervalValidator(Interval(8, 4096)),
     ),
     "grib-field-policy": _(
@@ -229,6 +234,7 @@ SETTINGS_AND_HELP = {
         """Maximum number of GRIB handles cached in memory per fieldlist with data on disk.
         Used when ``grib-handle-policy`` is ``cache``.
         See :doc:`/guide/misc/grib_memory` for more information.""",
+        getter="_as_int",
         none_ok=True,
     ),
     "use-grib-metadata-cache": _(
@@ -297,6 +303,10 @@ def save_settings(path, settings):
         pass
 
 
+def env_var_name(name):
+    return ENV_PREFIX + name.upper().replace("-", "_")
+
+
 class Settings:
     _auto_save_settings = True
     _notify_enabled = True
@@ -338,10 +348,14 @@ class Settings:
         else:
             getter = getattr(self, getter)
 
-        if default is NONE:
-            return getter(name, self._settings[name], none_ok)
+        st, value, _ = self._env(name)
+        if not st:
+            if default is NONE:
+                value = self._settings[name]
+            else:
+                value = self._settings.get(name, default)
 
-        return getter(name, self._settings.get(name, default), none_ok)
+        return getter(name, value, none_ok)
 
     @forward
     def set(self, *args, **kwargs):
@@ -380,6 +394,15 @@ class Settings:
         if name not in SETTINGS_AND_HELP:
             raise KeyError("No setting name '%s'" % (name,))
 
+        if self._has_env(name):
+            msg = (
+                f"Setting '{name}' is also set by environment variable '{env_var_name(name)}'."
+                "The environment variable takes precedence and its value is returned when calling get()."
+            )
+            if self._auto_save_settings:
+                msg += " Still, the value set here will be saved to the settings file."
+            warnings.warn(msg)
+
         settings_item = SETTINGS_AND_HELP[name]
 
         klass = settings_item.kind
@@ -446,27 +469,50 @@ class Settings:
 
             self._settings.pop(name, None)
             if name in DEFAULTS:
+                if self._has_env(name):
+                    msg = (
+                        f"Setting '{name}' is also set by environment variable '{env_var_name(name)}'."
+                        "The environment variable takes precedence and its value is returned when calling get()."
+                    )
+                    if self._auto_save_settings:
+                        msg += " Still, the value reset here will be saved to the settings file."
+                    warnings.warn(msg)
                 self._settings[name] = DEFAULTS[name]
         self._changed()
 
     @forward
     def __repr__(self):
         r = []
+        env = self.env()
         for k, v in sorted(self._settings.items()):
             setting = SETTINGS_AND_HELP.get(k, None)
             default = setting.default if setting else ""
-            r.append(f"{k}: ({v}, {default})")
+            if k in env:
+                r.append(f"{k}: ({env[k][0]}={env[k][1]}, {v}, {default})")
+            else:
+                r.append(f"{k}: ({v}, {default})")
         return "\n".join(r)
 
     @forward
     def _repr_html_(self):
         html = [css("table")]
         html.append("<table class='ek'>")
-        html.append("<tr><th>Name</th><th>Value</th><th>Default</th></tr>")
+
+        env = self.env()
+        columns = ["Name", "Value", "Default"]
+        html.append("<tr><th>%s</th></tr>" % ("</th><th>".join(columns)))
+
         for k, v in sorted(self._settings.items()):
             setting = SETTINGS_AND_HELP.get(k, None)
             default = setting.default if setting else ""
-            html.append("<tr><td>%s</td><td>%r</td><td>%r</td></tr>" % (k, v, default))
+            if k in env:
+                html.append(
+                    "<tr><td>%s</td><td>%s=%r<br>(%r)</td><td>%r</td></tr>"
+                    % (k, env[k][0], env[k][1], v, default)
+                )
+            else:
+                html.append("<tr><td>%s</td><td>%r</td><td>%r</td></tr>" % (k, v, default))
+
         html.append("</table>")
         return "".join(html)
 
@@ -524,8 +570,12 @@ class Settings:
             return None
         return str(value)
 
-    # def _as_number(self, name, value, units, none_ok):
-    #     return as_number(name, value, units, none_ok)
+    def _as_int(self, name, value, none_ok):
+        if value is None and none_ok:
+            return None
+        if isinstance(value, str):
+            value = value.replace('"', "").replace("'", "").strip()
+        return int(value)
 
     @forward
     def temporary(self, *args, **kwargs):
@@ -546,6 +596,25 @@ class Settings:
     def auto_save_settings(self, v):
         Settings._auto_save_settings = v
 
+    def env(self):
+        r = {}
+        for k in sorted(self._settings.keys()):
+            st, value, name = self._env(k)
+            if st:
+                r[k] = (name, value)
+        return r
+
+    @staticmethod
+    def _has_env(name):
+        return env_var_name(name) in os.environ
+
+    @staticmethod
+    def _env(name):
+        name = env_var_name(name)
+        if name in os.environ:
+            return (True, os.environ.get(name), name)
+        return (False, None, None)
+
 
 save = False
 settings_yaml = os.path.expanduser(os.path.join(DOT_EARTHKIT_DATA, "settings.yaml"))
diff --git a/src/earthkit/data/sphinxext/generate_settings_env_rst.py b/src/earthkit/data/sphinxext/generate_settings_env_rst.py
new file mode 100644
index 0000000..fdd93de
--- /dev/null
+++ b/src/earthkit/data/sphinxext/generate_settings_env_rst.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+# (C) Copyright 2020 ECMWF.
+#
+# This software is licensed under the terms of the Apache Licence Version 2.0
+# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
+# In applying this licence, ECMWF does not waive the privileges and immunities
+# granted to it by virtue of its status as an intergovernmental organisation
+# nor does it submit to any jurisdiction.
+#
+
+import re
+
+from earthkit.data.core.settings import SETTINGS_AND_HELP
+from earthkit.data.core.settings import env_var_name
+
+
+def execute(*args):
+    print()
+    print(".. list-table::")
+    print("   :header-rows: 1")
+    print("   :widths: 40 60")
+    print()
+    print("   * - Name")
+    print("     - Environment Variable")
+    print()
+    for k, v in sorted(SETTINGS_AND_HELP.items()):
+        if len(args) and not any(re.match(arg, k) for arg in args):
+            continue
+
+        print("   * -  ", k.replace("-", "\u2011"))  # Non-breaking hyphen
+        print("     -  ", f"{env_var_name(k)}")
+    print()
+
+
+if __name__ == "__main__":
+    execute()


Test patch:
diff --git a/tests/core/test_settings.py b/tests/core/test_settings.py
index e0ef2fa..5212fad 100644
--- a/tests/core/test_settings.py
+++ b/tests/core/test_settings.py
@@ -9,6 +9,7 @@
 # nor does it submit to any jurisdiction.
 #
 
+
 import pytest
 
 from earthkit.data import settings
@@ -218,6 +219,26 @@ def test_settings_auto_save_2():
     settings.auto_save_settings = v_ori
 
 
+@pytest.mark.parametrize(
+    "value,error", [("10000", None), (10000, None), ("1b", ValueError), ("A", ValueError)]
+)
+def test_settings_env(monkeypatch, value, error):
+    env_key = "EARTHKIT_DATA_NUMBER_OF_DOWNLOAD_THREADS"
+    monkeypatch.setenv(env_key, value)
+
+    v_ori = settings.auto_save_settings
+    settings.auto_save_settings = True
+
+    if error is None:
+        v = settings.get("number-of-download-threads")
+        assert v == 10000
+    else:
+        with pytest.raises(error):
+            settings.get("number-of-download-threads")
+
+    settings.auto_save_settings = v_ori
+
+
 if __name__ == "__main__":
     from earthkit.data.testing import main