Repository: eWaterCycle/era5cli

Issue:
Filenames do not reflect month selections
In era5cli, the filename will contain the source (e.g. `era5-land`), the variable name, the year, if the data is hourly, and the extent (e.g. `3E-8E_50N-54N`). However, the name does not contain the month(s). Users may choose to split requests up into single or multiple months per year to avoid making requests which are to large.

Currently this requires manually renaming the files after they have been downloaded, and before the next file is downloaded to avoid files being overwritten (see #76).


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/eWaterCycle/era5cli/pull/139?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=eWaterCycle) Report
> Merging [#139](https://codecov.io/gh/eWaterCycle/era5cli/pull/139?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=eWaterCycle) (8b14e44) into [main](https://codecov.io/gh/eWaterCycle/era5cli/commit/8fca8337a659e613d03cf6aeba2890c292b361e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=eWaterCycle) (8fca833) will **increase** coverage by `0.15%`.
> The diff coverage is `100.00%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #139      +/-   ##
==========================================
+ Coverage   99.24%   99.39%   +0.15%     
==========================================
  Files           7        8       +1     
  Lines         398      498     +100     
==========================================
+ Hits          395      495     +100     
  Misses          3        3              
```

</details>
BSchilperoort: @Peter9192, I have tackled your comments. Could you have another look?

Note that codecov is (randomly) failing due to an issue on their end. This is the coverage report summary:
```
Name                        Stmts   Miss  Cover
-----------------------------------------------
TOTAL                         541      1    99%
```
BSchilperoort: Thanks for the second look. I will:
- Change the --splitmonths from a flag to needing a value (ie. `--splitmonths True` vs `--splitmonths False`
- Make splitting by months the future default for all data

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3c45320..4182c5b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,6 +5,10 @@ Unreleased
 ~~~~~~~~~~
 * Change CDS keys from .cdsapirc file to .config/eracli.txt file. This will avoid conflict with e.g. ADS.
 * Add validator for era5cli.txt keys. This should provide better feedback to users and reduce user error.
+* Added --splitmonths argument for `era5cli hourly`. This allows users to avoid a Request Too Large error.
+* If a user makes a request without --splitmonths they are warned that the behavior will change in the future, and that they have to choose between --splitmonths False and --splitmonths True.
+* When a request would encounter a Request Too Large error in the CDS API, they are warned, and given a suggestion to use --splitmonths.
+* cli.py has been refactored to make the structure more clear. Seperate argument builders are now in their own modules.
 
 1.3.2 (2021-12-13)
 ~~~~~~~~~~~~~~~~~~
diff --git a/era5cli/__version__.py b/era5cli/__version__.py
index 9e51964..49afe43 100644
--- a/era5cli/__version__.py
+++ b/era5cli/__version__.py
@@ -23,6 +23,7 @@ __author__ = (
     "Stef Smeets",
     "Stefan Verhoeven",
     "Elizaveta Malinina",
+    "Bart Schilperoort",
 )
 __email__ = "ewatercycle@esciencecenter.nl"
 __version__ = "1.3.2"
diff --git a/era5cli/_request_size.py b/era5cli/_request_size.py
new file mode 100644
index 0000000..565d179
--- /dev/null
+++ b/era5cli/_request_size.py
@@ -0,0 +1,50 @@
+"""Module to compute the size of the CDS request."""
+
+from typing import TYPE_CHECKING
+from era5cli import inputref
+
+
+if TYPE_CHECKING:
+    from era5cli.fetch import Fetch
+
+
+# The following "MAX_REQUESTS" come from the cds.climate.copernicus.eu
+# api request generator. On the CDS one 'request' represents one 2D 'image' of the data
+# i.e. one variable, at one timestep, at one (pressure)level.
+# Single- and multi- level variables have a limit of 120.000 requests, while ERA5-land
+# is limited to 1000, likely due to the higher spatial resolution.
+MAX_REQUESTS = 120000  # Maximum requests for non-land data
+MAX_REQUESTS_LAND = 1000  # Max requests for "reanalysis-era5-land"
+VALID_HOURS_ENSEMBLE = [0, 3, 6, 9, 12, 15, 18, 21]
+
+
+class TooLargeRequestError(Exception):
+    """Raised when a request is too big for the CDS."""
+
+
+def n_hours(fetch: "Fetch") -> int:
+    """Get the number of hours in a request."""
+    if fetch.ensemble:
+        return sum(hr in fetch.hours for hr in VALID_HOURS_ENSEMBLE)
+    return len(fetch.hours)
+
+
+def request_too_large(fetch: "Fetch") -> bool:
+    """Determine if a request will raise a Too Large Request error at the CDS."""
+    n_months = 1 if fetch.splitmonths else len(fetch.months)
+
+    # Each time step counts as a request
+    request_size = n_months * len(fetch.days) * n_hours(fetch)
+
+    if fetch.land:
+        return True if request_size > MAX_REQUESTS_LAND else False
+
+    # Every pressure level is a separate request
+    if any([var in inputref.PLVARS for var in fetch.variables]):
+        request_size *= len(fetch.pressure_levels)
+
+    # Each (ensemble) statistic counts as a separate request
+    if fetch.statistics:
+        request_size *= 3  # Mean and spread are added.
+
+    return True if request_size > MAX_REQUESTS else False
diff --git a/era5cli/args/__init__.py b/era5cli/args/__init__.py
new file mode 100644
index 0000000..411041b
--- /dev/null
+++ b/era5cli/args/__init__.py
@@ -0,0 +1,7 @@
+from era5cli.args import common
+from era5cli.args import config
+from era5cli.args import info
+from era5cli.args import periods
+
+
+__all__ = ["common", "config", "periods", "info"]
diff --git a/era5cli/args/common.py b/era5cli/args/common.py
new file mode 100644
index 0000000..1b9f109
--- /dev/null
+++ b/era5cli/args/common.py
@@ -0,0 +1,268 @@
+import textwrap
+from argparse import ArgumentParser
+from datetime import datetime
+from typing import Union
+import era5cli.inputref as ref
+
+
+def _level_parse(level: str) -> Union[str, int]:
+    """Parse levels as integers, or the string 'surface'"""
+    return level if level == "surface" else int(level)
+
+
+def add_common_args(argument_parser: ArgumentParser) -> None:
+    """Populate the ArgumentParser with common (shared) arguments.
+
+    Adds the following arguments:
+        --variables,
+        --startyear,
+        --endyear,
+        --levels,
+        --outputprefix,
+        --format,
+        --merge,
+        --threads,
+        --ensemble,
+        --dryrun,
+        --prelimbe,
+        --land,
+        --area,
+
+    Args:
+        argument_parser: the ArgumentParser that the arguments are added to.
+    """
+    argument_parser.add_argument(
+        "--variables",
+        type=str,
+        required=True,
+        nargs="+",
+        help=textwrap.dedent(
+            """
+            The variables to download data for. This can be a
+            single variable, or multiple. See the Copernicus
+            Climate Data Store website or run
+            `era5cli info -h` for available variables.
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--startyear",
+        type=int,
+        required=True,
+        help=textwrap.dedent(
+            """
+            Single year or first year of range for which
+            data should be downloaded.
+            Every year will be downloaded in a separate file
+            by default. Set `--split false` to change this
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--endyear",
+        type=int,
+        required=False,
+        default=None,
+        help=textwrap.dedent(
+            """
+            Last year of range for which data should be
+            downloaded.
+            If only a single year is needed, only
+            `--startyear` needs to be specified.
+            Every year will be downloaded in a separate file
+            by default. Set `--split false` to change this
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--levels",
+        nargs="+",
+        type=_level_parse,
+        required=False,
+        default=ref.PLEVELS,
+        help=textwrap.dedent(
+            """
+            Pressure level(s) to download 3D variables for.
+            Default is all available levels. See the Copernicus
+            Climate Data Store website or run `era5cli info -h`
+            for available pressure levels. For geopotential,
+            `--levels surface` can be used to request data from
+            the single level dataset (previously called
+            orography)
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--outputprefix",
+        type=str,
+        default="era5",
+        help=textwrap.dedent(
+            """
+            Prefix to be used for the output filename.
+            Default prefix is `era5`
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--format",
+        type=str,
+        default="netcdf",
+        choices=["netcdf", "grib"],
+        help=textwrap.dedent(
+            """
+            Output file type. Defaults to `netcdf`
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--merge",
+        action="store_true",
+        default=False,
+        help=textwrap.dedent(
+            """
+            Merge yearly output files.
+            Default is split output files into separate files
+            for every year
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--threads",
+        type=int,
+        choices=range(1, 7),
+        required=False,
+        default=None,
+        help=textwrap.dedent(
+            """
+            Number of parallel threads to use when
+            downloading. Defaults to a single process
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--ensemble",
+        action="store_true",
+        default=False,
+        help=textwrap.dedent(
+            """
+            Whether to download high resolution realisation
+            (HRES) or a reduced resolution ten member ensemble
+            (EDA). Providing the `--ensemble` argument
+            downloads the reduced resolution ensemble.
+            `--ensemble` is incompatible with `--land`
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--dryrun",
+        action="store_true",
+        default=False,
+        help=textwrap.dedent(
+            """
+            Whether to print the cdsapi request to the screen,
+            or make the request to start downloading the data.
+            Providing the `--dryrun` argument will print the
+            request to stdout. By default, the data will be
+            downloaded
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--prelimbe",
+        action="store_true",
+        default=False,
+        help=textwrap.dedent(
+            """
+            Whether to download the preliminary back extension
+            (1950-1978). Note that when `--prelimbe` is used,
+            `--startyear` and `--endyear` should be set
+            between 1950 and 1978. Please, be aware that
+            ERA5 data is available from 1959.
+            `--prelimbe` is incompatible with `--land`
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--land",
+        action="store_true",
+        default=False,
+        help=textwrap.dedent(
+            """
+            Whether to download data from the ERA5-Land
+            dataset. Note that the ERA5-Land dataset starts in
+            1950.
+            `--land` is incompatible with the use of
+            `--prelimbe` and `--ensemble`
+
+            """
+        ),
+    )
+
+    argument_parser.add_argument(
+        "--area",
+        nargs=4,
+        type=float,
+        metavar=("LAT_MAX", "LON_MIN", "LAT_MIN", "LON_MAX"),
+        required=False,
+        help=textwrap.dedent(
+            """
+            Coordinates in case extraction of a subregion is
+            requested.
+            Specified as `LAT_MAX LON_MIN LAT_MIN LON_MAX`
+            (counterclockwise coordinates, starting at the top)
+            with longitude in the range -180, +180
+            and latitude in the range -90, +90. For example:
+            `--area 90 -180 -90 180`. Requests are rounded down
+            to two decimals. By default, the entire
+            available area will be returned
+
+            """
+        ),
+    )
+
+
+def construct_year_list(args):
+    """Make a continous list of years from the startyear and endyear arguments."""
+    if not args.endyear:
+        endyear = args.startyear
+    else:
+        endyear = args.endyear
+
+    # check whether correct years have been entered
+    for year in (args.startyear, endyear):
+        if args.prelimbe:
+            assert 1950 <= year <= 1978, "year should be between 1950 and 1978"
+        elif args.land:
+            assert (
+                1950 <= year <= datetime.now().year
+            ), "for ERA5-Land, year should be between 1950 and present"
+        else:
+            assert (
+                1959 <= year <= datetime.now().year
+            ), "year should be between 1959 and present"
+
+    assert endyear >= args.startyear, "endyear should be >= startyear or None"
+
+    return list(range(args.startyear, endyear + 1))
diff --git a/era5cli/args/config.py b/era5cli/args/config.py
new file mode 100644
index 0000000..2e947b2
--- /dev/null
+++ b/era5cli/args/config.py
@@ -0,0 +1,127 @@
+import argparse
+import textwrap
+from era5cli import key_management
+
+
+def add_config_args(subparsers: argparse._SubParsersAction) -> None:
+    """Populate the subparsers with the 'config' parser.
+
+    The config parser allows users to view as well as set their cds API keys.
+
+    Adds the 'config' parser with the following arguments:
+        --show
+        --uid
+        --key
+        --url
+
+    Args:
+        subparsers: Subparsers to which the 'config' parser should be added to.
+    """
+    config = subparsers.add_parser(
+        "config",
+        description="",
+        prog=textwrap.dedent(
+            """
+            Configure the CDS login info for era5cli.
+
+            This will create a config file in your home directory, in folder named
+            ".config". The CDS URL, your UID and the CDS keys will be stored here.
+
+            To find your key and UID, go to https://cds.climate.copernicus.eu/ and
+            login with your email and password. Then go to your user profile (top
+            right).
+
+            Use `era5cli config --help` for more information.
+            """
+        ),
+        help=textwrap.dedent(
+            """
+            Configure the CDS login info for era5cli.
+
+            """
+        ),
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+
+    config.add_argument(
+        "--show",
+        action="store_true",
+        default=False,
+        help=textwrap.dedent(
+            """
+            Print the stored keys to the screen.
+            """
+        ),
+    )
+
+    config.add_argument(
+        "--uid",
+        type=str,
+        help=textwrap.dedent(
+            """
+            Your CDS User ID, e.g.: 123456
+            """
+        ),
+    )
+
+    config.add_argument(
+        "--key",
+        type=str,
+        help=textwrap.dedent(
+            """
+            Your CDS key, e.g.: "4s215sgs-2dfa-6h34-62h2-1615ad163414"
+            """
+        ),
+    )
+
+    config.add_argument(
+        "--url",
+        type=str,
+        required=False,
+        default=key_management.DEFAULT_CDS_URL,
+        help=textwrap.dedent(
+            f"""
+            (optional) URL to the CDS, by default:
+                {key_management.DEFAULT_CDS_URL}
+            """
+        ),
+    )
+
+
+class InputError(Exception):
+    "Raised when a user inputs an invalid combination of arguments."
+
+
+def run_config(args):
+    """Control flow for the config subparser.
+
+    This custom control flow is required to implement the exclusive
+    groups [show] and [uid + key (+ url)], and specifies the behavior
+    of these arguments.
+
+    Args:
+        args: Arguments collected by argparse
+
+    Returns:
+        True
+    """
+    if args.show and any((args.uid, args.key)):
+        raise InputError("Either call `show` or set the key. Not both.")
+    if not args.show and (args.uid is None or args.key is None):
+        raise InputError("Both the UID and the key are required inputs.")
+    if args.show:
+        url, fullkey = key_management.load_era5cli_config()
+        uid, key = fullkey.split(":")
+        print(
+            "Contents of .config/era5cli.txt:\n"
+            f"    uid: {uid}\n"
+            f"    key: {key}\n"
+            f"    url: {url}\n"
+        )
+        return True
+
+    return key_management.set_config(
+        url=args.url,
+        uid=args.uid,
+        key=args.key,
+    )
diff --git a/era5cli/args/info.py b/era5cli/args/info.py
new file mode 100644
index 0000000..78266bb
--- /dev/null
+++ b/era5cli/args/info.py
@@ -0,0 +1,59 @@
+import argparse
+import textwrap
+import era5cli.info
+
+
+def add_info_args(subparsers):
+    """Add info parser and arguments.
+
+    Adds the 'info' parser, with the 'name' argument.
+    """
+    info = subparsers.add_parser(
+        "info",
+        description="Show information on available variables and levels.",
+        prog=textwrap.dedent(
+            """
+            Use `era5cli info --help` for more information
+
+            """
+        ),
+        help=textwrap.dedent(
+            """
+            Show information on available variables or levels.
+            Use `era5cli info --help` for more information
+
+            """
+        ),
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+
+    info.add_argument(
+        "name",
+        type=str,
+        help=textwrap.dedent(
+            """
+            Enter list name to print info list: \n
+            `levels` for all available pressure levels \n
+            `2Dvars` for all available single level or 2D
+            variables \n
+            `3Dvars` for all available 3D variables \n
+            `land` for all available variables in
+            ERA5-land \n
+            Enter variable name (e.g. `total_precipitation`)
+            or pressure level (e.g. `825`) to show if the
+            variable or level is available, and in which list
+
+            """
+        ),
+    )
+
+
+def run_info(args):
+    """Run the info routine"""
+    # List dataset information
+    era5info = era5cli.info.Info(args.name)
+    if era5info.infotype == "list":
+        era5info.list()
+        return True
+    era5info.vars()
+    return True
diff --git a/era5cli/args/periods.py b/era5cli/args/periods.py
new file mode 100644
index 0000000..8e6e8f6
--- /dev/null
+++ b/era5cli/args/periods.py
@@ -0,0 +1,200 @@
+import argparse
+import distutils.util
+import logging
+import textwrap
+
+
+def add_period_args(subparsers, common):
+    """Add period related parsers and arguments.
+
+    Adds the following parsers:
+        monthly, hourly.
+
+    As well as the following arguments (for
+    some of the previously mentioned parsers):
+        --months, --days, --hours, --synoptic, --statistics
+    """
+    mnth = argparse.ArgumentParser(add_help=False)
+
+    mnth.add_argument(
+        "--months",
+        nargs="+",
+        required=False,
+        type=int,
+        default=list(range(1, 13)),
+        help=textwrap.dedent(
+            """
+            Month(s) to download data for. Defaults to all
+            months. For every year, only these
+            months will be downloaded
+
+            """
+        ),
+    )
+
+    day = argparse.ArgumentParser(add_help=False)
+
+    day.add_argument(
+        "--days",
+        nargs="+",
+        required=False,
+        type=int,
+        default=list(range(1, 32)),
+        help=textwrap.dedent(
+            """
+            Day(s) to download data for. Defaults to all days.
+            For every year, only these days will
+            be downloaded
+
+            """
+        ),
+    )
+
+    hour = argparse.ArgumentParser(add_help=False)
+
+    hour.add_argument(
+        "--hours",
+        nargs="+",
+        required=False,
+        type=int,
+        default=list(range(24)),
+        help=textwrap.dedent(
+            """
+            Time of day in hours to download data for.
+            Defaults to all hours. For every year,
+            only these hours will be downloaded
+
+            """
+        ),
+    )
+
+    splitmonths = argparse.ArgumentParser(add_help=False)
+
+    splitmonths.add_argument(
+        "--splitmonths",
+        type=lambda x: bool(distutils.util.strtobool(x)),  # type=bool doesn't work.
+        default=None,  # To be set to True in the future
+        help=textwrap.dedent(
+            """
+            When downloading hourly data, use:
+            `--splitmonths True` to split requests and files
+            by month, and add the month to the filename.
+
+            Defaults to `False`, but will default to `True` in
+            a future release.
+            """
+        ),
+    )
+
+    hourly = subparsers.add_parser(
+        "hourly",
+        parents=[common, mnth, day, hour, splitmonths],
+        description="Execute the data fetch process for hourly data.",
+        prog=textwrap.dedent(
+            """
+            Use `era5cli hourly --help` for more information
+
+            """
+        ),
+        help=textwrap.dedent(
+            """
+            Execute the data fetch process for hourly data.
+            Use `era5cli hourly --help` for more information
+
+            """
+        ),
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+
+    hourly.add_argument(
+        "--statistics",
+        action="store_true",
+        help=textwrap.dedent(
+            """
+            When downloading hourly ensemble data, provide
+            the `--statistics` argument to download statistics
+            (ensemble mean and ensemble spread)
+
+            """
+        ),
+    )
+
+    monthly = subparsers.add_parser(
+        "monthly",
+        parents=[common, mnth],
+        description="Execute the data fetch process for monthly data.",
+        prog=textwrap.dedent(
+            """
+            Use `era5cli monthly --help` for more information
+
+            """
+        ),
+        help=textwrap.dedent(
+            """
+            Execute the data fetch process for monthly data.
+            Use `era5cli monthly --help` for more information
+
+            """
+        ),
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+
+    monthly.add_argument(
+        "--synoptic",
+        type=int,
+        default=False,
+        nargs="*",
+        help=textwrap.dedent(
+            """
+            Time of day in hours to get the synoptic means
+            (monthly averaged by hour of day) for. For example
+            `--synoptic 0 4 5 6 23`. Give empty option
+            `--synoptic` to download all hours (0-23).
+            The option defaults to `None` in which case the
+            monthly average of daily means is chosen
+
+            """
+        ),
+    )
+
+
+def set_period_args(args):
+    """Set subroutine specific arguments for monthly and hourly fetches."""
+    if args.command == "monthly":
+        statistics = None
+        days = None
+        splitmonths = False
+        if args.synoptic is False:
+            synoptic = None
+            hours = [0]
+        elif len(args.synoptic) == 0:
+            synoptic = True
+            hours = range(24)
+        else:
+            synoptic = True
+            hours = args.synoptic
+    elif args.command == "hourly":
+        synoptic = None
+        if args.splitmonths is None:
+            splitmonths = False
+            logging.warning(
+                "\n  The argument --splitmonths was not used. However, in a future "
+                "\n  version this flag will default to `True`. To avoid this, either"
+                "\n  use `--splitmonths True` and update your workflow accordingly,"
+                "\n  or set --splitmonths to False."
+            )
+        else:
+            splitmonths: bool = args.splitmonths
+        print(splitmonths)
+
+        statistics: bool = args.statistics
+        if statistics:
+            assert args.ensemble, (
+                "Statistics can only be computed over an ensemble, "
+                "add --ensemble or remove --statistics."
+            )
+        days = args.days
+        hours = args.hours
+    else:
+        raise AttributeError(f'The command "{args.command}" is not valid.')
+    return synoptic, statistics, splitmonths, days, hours
diff --git a/era5cli/cli.py b/era5cli/cli.py
index f1a0ed7..4a2d2c6 100644
--- a/era5cli/cli.py
+++ b/era5cli/cli.py
@@ -3,19 +3,8 @@
 
 import argparse
 import sys
-import textwrap
-from datetime import datetime
 import era5cli.fetch as efetch
-import era5cli.info as einfo
-import era5cli.inputref as ref
-from era5cli import key_management
-
-
-def _level_parse(level):
-    """Parse levels as integers, or the string 'surface'"""
-    if level == "surface":
-        return str(level)
-    return int(level)
+from era5cli import args
 
 
 def _build_parser():
@@ -28,440 +17,13 @@ def _build_parser():
     subparsers.required = True
 
     common = argparse.ArgumentParser(add_help=False)
+    args.common.add_common_args(common)
 
-    common.add_argument(
-        "--variables",
-        type=str,
-        required=True,
-        nargs="+",
-        help=textwrap.dedent(
-            """
-            The variables to download data for. This can be a
-            single variable, or multiple. See the Copernicus
-            Climate Data Store website or run
-            `era5cli info -h` for available variables.
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--startyear",
-        type=int,
-        required=True,
-        help=textwrap.dedent(
-            """
-            Single year or first year of range for which
-            data should be downloaded.
-            Every year will be downloaded in a separate file
-            by default. Set `--split false` to change this
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--endyear",
-        type=int,
-        required=False,
-        default=None,
-        help=textwrap.dedent(
-            """
-            Last year of range for which data should be
-            downloaded.
-            If only a single year is needed, only
-            `--startyear` needs to be specified.
-            Every year will be downloaded in a separate file
-            by default. Set `--split false` to change this
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--levels",
-        nargs="+",
-        type=_level_parse,
-        required=False,
-        default=ref.PLEVELS,
-        help=textwrap.dedent(
-            """
-            Pressure level(s) to download 3D variables for.
-            Default is all available levels. See the Copernicus
-            Climate Data Store website or run `era5cli info -h`
-            for available pressure levels. For geopotential,
-            `--levels surface` can be used to request data from
-            the single level dataset (previously called
-            orography)
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--outputprefix",
-        type=str,
-        default="era5",
-        help=textwrap.dedent(
-            """
-            Prefix to be used for the output filename.
-            Default prefix is `era5`
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--format",
-        type=str,
-        default="netcdf",
-        choices=["netcdf", "grib"],
-        help=textwrap.dedent(
-            """
-            Output file type. Defaults to `netcdf`
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--merge",
-        action="store_true",
-        default=False,
-        help=textwrap.dedent(
-            """
-            Merge yearly output files.
-            Default is split output files into separate files
-            for every year
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--threads",
-        type=int,
-        choices=range(1, 7),
-        required=False,
-        default=None,
-        help=textwrap.dedent(
-            """
-            Number of parallel threads to use when
-            downloading. Defaults to a single process
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--ensemble",
-        action="store_true",
-        default=False,
-        help=textwrap.dedent(
-            """
-            Whether to download high resolution realisation
-            (HRES) or a reduced resolution ten member ensemble
-            (EDA). Providing the `--ensemble` argument
-            downloads the reduced resolution ensemble.
-            `--ensemble` is incompatible with `--land`
+    args.periods.add_period_args(subparsers, common)
 
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--dryrun",
-        action="store_true",
-        default=False,
-        help=textwrap.dedent(
-            """
-            Whether to print the cdsapi request to the screen,
-            or make the request to start downloading the data.
-            Providing the `--dryrun` argument will print the
-            request to stdout. By default, the data will be
-            downloaded
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--prelimbe",
-        action="store_true",
-        default=False,
-        help=textwrap.dedent(
-            """
-            Whether to download the preliminary back extension
-            (1950-1978). Note that when `--prelimbe` is used,
-            `--startyear` and `--endyear` should be set
-            between 1950 and 1978. Please, be aware that
-            ERA5 data is available from 1959.
-            `--prelimbe` is incompatible with `--land`
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--land",
-        action="store_true",
-        default=False,
-        help=textwrap.dedent(
-            """
-            Whether to download data from the ERA5-Land
-            dataset. Note that the ERA5-Land dataset starts in
-            1950.
-            `--land` is incompatible with the use of
-            `--prelimbe` and `--ensemble`
-
-            """
-        ),
-    )
-
-    common.add_argument(
-        "--area",
-        nargs=4,
-        type=float,
-        metavar=("LAT_MAX", "LON_MIN", "LAT_MIN", "LON_MAX"),
-        required=False,
-        help=textwrap.dedent(
-            """
-            Coordinates in case extraction of a subregion is
-            requested.
-            Specified as `LAT_MAX LON_MIN LAT_MIN LON_MAX`
-            (counterclockwise coordinates, starting at the top)
-            with longitude in the range -180, +180
-            and latitude in the range -90, +90. For example:
-            `--area 90 -180 -90 180`. Requests are rounded down
-            to two decimals. By default, the entire
-            available area will be returned
-
-            """
-        ),
-    )
-
-    mnth = argparse.ArgumentParser(add_help=False)
-
-    mnth.add_argument(
-        "--months",
-        nargs="+",
-        required=False,
-        type=int,
-        default=list(range(1, 13)),
-        help=textwrap.dedent(
-            """
-            Month(s) to download data for. Defaults to all
-            months. For every year, only these
-            months will be downloaded
-
-            """
-        ),
-    )
-
-    day = argparse.ArgumentParser(add_help=False)
-
-    day.add_argument(
-        "--days",
-        nargs="+",
-        required=False,
-        type=int,
-        default=list(range(1, 32)),
-        help=textwrap.dedent(
-            """
-            Day(s) to download data for. Defaults to all days.
-            For every year, only these days will
-            be downloaded
-
-            """
-        ),
-    )
-
-    hour = argparse.ArgumentParser(add_help=False)
-
-    hour.add_argument(
-        "--hours",
-        nargs="+",
-        required=False,
-        type=int,
-        default=list(range(0, 24)),
-        help=textwrap.dedent(
-            """
-            Time of day in hours to download data for.
-            Defaults to all hours. For every year,
-            only these hours will be downloaded
-
-            """
-        ),
-    )
+    args.info.add_info_args(subparsers)
 
-    hourly = subparsers.add_parser(
-        "hourly",
-        parents=[common, mnth, day, hour],
-        description="Execute the data fetch process for hourly data.",
-        prog=textwrap.dedent(
-            """
-            Use `era5cli hourly --help` for more information
-
-            """
-        ),
-        help=textwrap.dedent(
-            """
-            Execute the data fetch process for hourly data.
-            Use `era5cli hourly --help` for more information
-
-            """
-        ),
-        formatter_class=argparse.RawTextHelpFormatter,
-    )
-
-    hourly.add_argument(
-        "--statistics",
-        action="store_true",
-        help=textwrap.dedent(
-            """
-            When downloading hourly ensemble data, provide
-            the `--statistics` argument to download statistics
-            (ensemble mean and ensemble spread)
-
-            """
-        ),
-    )
-
-    monthly = subparsers.add_parser(
-        "monthly",
-        parents=[common, mnth],
-        description="Execute the data fetch process for monthly data.",
-        prog=textwrap.dedent(
-            """
-            Use `era5cli monthly --help` for more information
-
-            """
-        ),
-        help=textwrap.dedent(
-            """
-            Execute the data fetch process for monthly data.
-            Use `era5cli monthly --help` for more information
-
-            """
-        ),
-        formatter_class=argparse.RawTextHelpFormatter,
-    )
-
-    monthly.add_argument(
-        "--synoptic",
-        type=int,
-        default=False,
-        nargs="*",
-        help=textwrap.dedent(
-            """
-            Time of day in hours to get the synoptic means
-            (monthly averaged by hour of day) for. For example
-            `--synoptic 0 4 5 6 23`. Give empty option
-            `--synoptic` to download all hours (0-23).
-            The option defaults to `None` in which case the
-            monthly average of daily means is chosen
-
-            """
-        ),
-    )
-
-    info = subparsers.add_parser(
-        "info",
-        description="Show information on available variables and levels.",
-        prog=textwrap.dedent(
-            """
-            Use `era5cli info --help` for more information
-
-            """
-        ),
-        help=textwrap.dedent(
-            """
-            Show information on available variables or levels.
-            Use `era5cli info --help` for more information
-
-            """
-        ),
-        formatter_class=argparse.RawTextHelpFormatter,
-    )
-
-    info.add_argument(
-        "name",
-        type=str,
-        help=textwrap.dedent(
-            """
-            Enter list name to print info list: \n
-            `levels` for all available pressure levels \n
-            `2Dvars` for all available single level or 2D
-            variables \n
-            `3Dvars` for all available 3D variables \n
-            `land` for all available variables in
-            ERA5-land \n
-            Enter variable name (e.g. `total_precipitation`)
-            or pressure level (e.g. `825`) to show if the
-            variable or level is available, and in which list
-
-            """
-        ),
-    )
-
-    config = subparsers.add_parser(
-        "config",
-        description="",
-        prog=textwrap.dedent(
-            """
-            Configure the CDS login info for era5cli.
-
-            This will create a config file in your home directory, in folder named
-            ".config". The CDS URL, your UID and the CDS keys will be stored here.
-
-            To find your key and UID, go to https://cds.climate.copernicus.eu/ and login
-            with your email and password. Then go to your user profile (top right).
-
-            Use `era5cli config --help` for more information.
-            """
-        ),
-        help=textwrap.dedent(
-            """
-            Configure the CDS login info for era5cli.
-
-            """
-        ),
-        formatter_class=argparse.RawTextHelpFormatter,
-    )
-
-    config.add_argument(
-        "--uid",
-        type=str,
-        required=True,
-        help=textwrap.dedent(
-            """
-            Your CDS User ID, e.g.: 123456
-            """
-        ),
-    )
-
-    config.add_argument(
-        "--key",
-        type=str,
-        required=True,
-        help=textwrap.dedent(
-            """
-            Your CDS key, e.g.: "4s215sgs-2dfa-6h34-62h2-1615ad163414"
-            """
-        ),
-    )
-
-    config.add_argument(
-        "--url",
-        type=str,
-        required=False,
-        default=key_management.DEFAULT_CDS_URL,
-        help=textwrap.dedent(
-            f"""
-            (optional) URL to the CDS, by default: {key_management.DEFAULT_CDS_URL}
-            """
-        ),
-    )
+    args.config.add_config_args(subparsers)
 
     return parser
 
@@ -472,117 +34,60 @@ def _parse_args(args):
     return parser.parse_args(args)
 
 
-def _run_info(args):
-    # List dataset information
-    era5info = einfo.Info(args.name)
-    if era5info.infotype == "list":
-        era5info.list()
-        return True
-    era5info.vars()
-    return True
-
+def _execute(input_args: argparse.Namespace) -> True:
+    """Execute the arguments given by the user."""
 
-def _construct_year_list(args):
-    if not args.endyear:
-        endyear = args.startyear
-    else:
-        endyear = args.endyear
+    if input_args.command == "info":
+        return args.info.run_info(input_args)
 
-    # check whether correct years have been entered
-    for year in (args.startyear, endyear):
-        if args.prelimbe:
-            assert 1950 <= year <= 1978, "year should be between 1950 and 1978"
-        elif args.land:
-            assert (
-                1950 <= year <= datetime.now().year
-            ), "for ERA5-Land, year should be between 1950 and present"
-        else:
-            assert (
-                1959 <= year <= datetime.now().year
-            ), "year should be between 1959 and present"
-
-    assert endyear >= args.startyear, "endyear should be >= startyear or None"
-
-    return list(range(args.startyear, endyear + 1))
-
-
-def _set_period_args(args):
-    # set subroutine specific arguments for monthly and hourly fetch
-    if args.command == "monthly":
-        statistics = None
-        days = None
-        if args.synoptic is False:
-            synoptic = None
-            hours = [0]
-        elif len(args.synoptic) == 0:
-            synoptic = True
-            hours = range(24)
-        else:
-            synoptic = True
-            hours = args.synoptic
-    elif args.command == "hourly":
-        synoptic = None
-        statistics = args.statistics
-        if statistics:
-            assert args.ensemble, (
-                "Statistics can only be computed over an ensemble, "
-                "add --ensemble or remove --statistics."
-            )
-        days = args.days
-        hours = args.hours
-    else:
-        raise AttributeError(f'The command "{args.command}" is not valid.')
-    return synoptic, statistics, days, hours
-
-
-def _execute(args):
-    """Call to ERA-5 cli library."""
-    # the info subroutine
-    if args.command == "info":
-        return _run_info(args)
-
-    if args.command == "config":
-        return key_management.run_config(
-            url=args.url,
-            uid=args.uid,
-            key=args.key,
-        )
+    if input_args.command == "config":
+        return args.config.run_config(input_args)
 
     # the fetching subroutines
-    years = _construct_year_list(args)
-    synoptic, statistics, days, hours = _set_period_args(args)
+    years = args.common.construct_year_list(input_args)
+    synoptic, statistics, splitmonths, days, hours = args.periods.set_period_args(
+        input_args
+    )
+
     # try to build and send download request
     era5 = efetch.Fetch(
         years,
-        months=args.months,
+        months=input_args.months,
         days=days,
         hours=hours,
-        variables=args.variables,
-        area=args.area,
-        outputformat=args.format,
-        outputprefix=args.outputprefix,
-        period=args.command,
-        ensemble=args.ensemble,
+        variables=input_args.variables,
+        area=input_args.area,
+        outputformat=input_args.format,
+        outputprefix=input_args.outputprefix,
+        period=input_args.command,
+        ensemble=input_args.ensemble,
         synoptic=synoptic,
         statistics=statistics,
-        pressurelevels=args.levels,
-        threads=args.threads,
-        merge=args.merge,
-        prelimbe=args.prelimbe,
-        land=args.land,
-    )
-    era5.fetch(dryrun=args.dryrun)
+        pressurelevels=input_args.levels,
+        threads=input_args.threads,
+        splitmonths=splitmonths,
+        merge=input_args.merge,
+        prelimbe=input_args.prelimbe,
+        land=input_args.land,
+    )
+    era5.fetch(dryrun=input_args.dryrun)
     return True
 
 
 def main(argv=None):
-    """Main."""
-    # get arguments
+    """Run era5cli.
+
+    argv is an optional kwarg to be used in testing. When called
+    from the command line, the user-input arguments are retreived
+    using sys.argv.
+    """
     if argv is None:
-        argv = sys.argv
+        argv = sys.argv  # pragma: no cover
+
+    # Skip the first argument (i.e. 'era5cli')
     args = _parse_args(argv[1:])
     _execute(args)
 
 
 if __name__ == "__main__":
-    main()
+    main()  # pragma: no cover
diff --git a/era5cli/fetch.py b/era5cli/fetch.py
index 0353d44..a2303e7 100644
--- a/era5cli/fetch.py
+++ b/era5cli/fetch.py
@@ -1,5 +1,6 @@
 """Fetch ERA5 variables."""
 
+import itertools
 import logging
 import os
 import cdsapi
@@ -7,6 +8,8 @@ from pathos.threading import ThreadPool as Pool
 import era5cli.inputref as ref
 import era5cli.utils
 from era5cli import key_management
+from era5cli._request_size import TooLargeRequestError
+from era5cli._request_size import request_too_large
 
 
 class Fetch:
@@ -100,6 +103,7 @@ class Fetch:
         statistics=None,
         synoptic=None,
         pressurelevels=None,
+        splitmonths=False,
         merge=False,
         threads=None,
         prelimbe=False,
@@ -137,6 +141,8 @@ class Fetch:
         """str: Prefix of output filename."""
         self.threads = threads
         """int: number of parallel threads to use for downloading."""
+        self.splitmonths = splitmonths
+        """bool: Split request per month, can avoid Too Large Request error."""
         self.merge = merge
         """bool: Merge yearly output files if True."""
         self.period = period
@@ -161,6 +167,27 @@ class Fetch:
         """bool: Whether to download from the ERA5-Land
         dataset."""
 
+        if self.merge and self.splitmonths:
+            raise ValueError(
+                "\nThe commands '--merge' and '--splitmonths' are not compatible with"
+                "\neach other. Please pick one of the two."
+            )
+
+        vars = list(self.variables)  # Use list() to avoid copying by reference
+        if "geopotential" in vars and pressurelevels == ["surface"]:
+            vars.remove("geopotential")
+        if any([var in ref.PLVARS for var in vars]):
+            print(pressurelevels)
+            self._check_levels()
+
+        if self.period == "hourly" and request_too_large(self):
+            raise TooLargeRequestError(
+                "\n  Your request is too large for the CDS API."
+                "\n  Consider splitting up your request in months, "
+                "\n  by using '--splitmonths True'."
+                "\n  For more info see 'era5cli hourly --help'."
+            )
+
     def _get_login(self):
         self.url, self.key = key_management.load_era5cli_config()
 
@@ -178,11 +205,12 @@ class Fetch:
         # define extension output filename
         self._extension()
         # define fetch call depending on split argument
-        if not self.merge:
-            # split by variable and year
+
+        if self.splitmonths:
+            self._split_variable_yr_month()
+        elif not self.merge:
             self._split_variable_yr()
         else:
-            # split by variable
             self._split_variable()
 
     def _extension(self):
@@ -206,13 +234,19 @@ class Fetch:
         name = f"_{lon(lon_min)}-{lon(lon_max)}_{lat(lat_min)}-{lat(lat_max)}"
         return name
 
-    def _define_outputfilename(self, var, years):
+    def _define_outputfilename(self, var, years, month=None):
         """Define output filename."""
         start, end = years[0], years[-1]
 
         prefix = f"{self.outputprefix}-land" if self.land else self.outputprefix
-        yearblock = f"{start}-{end}" if not start == end else f"{start}"
-        fname = f"{prefix}_{var}_{yearblock}_{self.period}"
+
+        yearblock = f"{start}-{end}" if start != end else f"{start}"
+
+        fname = f"{prefix}_{var}_{yearblock}"
+        if month is not None:
+            fname += f"-{month}"
+        fname += f"_{self.period}"
+
         if self.area:
             fname += self._process_areaname()
         if self.ensemble:
@@ -247,6 +281,24 @@ class Fetch:
         pool = Pool(nodes=self.threads) if self.threads else Pool()
         pool.map(self._getdata, variables, years, outputfiles)
 
+    def _split_variable_yr_month(self):
+        """Fetch variable split by variable, year, and month."""
+        outputfiles = []
+        variables = []
+        years = []
+        months = []
+
+        for var, year, month in itertools.product(
+            self.variables, self.years, self.months
+        ):
+            outputfiles += [self._define_outputfilename(var, [year, year], month)]
+            variables += [var]
+            years += [year]
+            months += [month]
+
+        pool = Pool(nodes=self.threads) if self.threads else Pool()
+        pool.map(self._getdata, variables, years, outputfiles, months)
+
     def _product_type(self):
         """Construct the product type name from the options."""
         assert not (
@@ -381,9 +433,9 @@ class Fetch:
             variable = "geopotential"
             name += "-single-levels"
             logging.warning(
-                "The variable 'orography' has been deprecated by CDS. Use "
-                "`--variables geopotential --levels surface` going forward. "
-                "The current query has been changed accordingly."
+                "\n  The variable 'orography' has been deprecated by CDS. Use"
+                "\n  `--variables geopotential --levels surface` going forward."
+                "\n  The current query has been changed accordingly."
             )
         elif self.pressure_levels == ["surface"]:
             name += "-single-levels"
@@ -406,7 +458,7 @@ class Fetch:
             name += "-preliminary-back-extension"
         return name, variable
 
-    def _build_request(self, variable, years):
+    def _build_request(self, variable, years, months=None):
         """Build the download request for the retrieve method of cdsapi."""
         self._check_variable(variable)
 
@@ -415,13 +467,12 @@ class Fetch:
         request = {
             "variable": variable,
             "year": years,
-            "month": self.months,
+            "month": self.months if months is None else months,
             "time": self.hours,
             "format": self.outputformat,
         }
 
         if "pressure-levels" in name:
-            self._check_levels()
             request["pressure_level"] = self.pressure_levels
 
         if self.area:
@@ -439,9 +490,9 @@ class Fetch:
     def _exit(self):
         pass
 
-    def _getdata(self, variables: list, years: list, outputfile: str):
+    def _getdata(self, variables: list, years: list, outputfile: str, months=None):
         """Fetch variables using cds api call."""
-        name, request = self._build_request(variables, years)
+        name, request = self._build_request(variables, years, months)
         if self.dryrun:
             print(name, request, outputfile)
         else:
@@ -453,7 +504,7 @@ class Fetch:
                 "please do not kill this process in the meantime.",
                 os.linesep,
             )
-            connection = cdsapi.Client(url=self.url, key=self.key)
+            connection = cdsapi.Client(url=self.url, key=self.key, verify=True)
             print("".join(queueing_message))  # print queueing message
             connection.retrieve(name, request, outputfile)
             era5cli.utils.append_history(name, request, outputfile)
diff --git a/era5cli/key_management.py b/era5cli/key_management.py
index c97279e..86d319d 100644
--- a/era5cli/key_management.py
+++ b/era5cli/key_management.py
@@ -86,7 +86,7 @@ def attempt_cds_login(url: str, fullkey: str) -> True:
         raise err  # pragma: no cover
 
 
-def run_config(
+def set_config(
     url: str,
     uid: str,
     key: str,
diff --git a/pyproject.toml b/pyproject.toml
index 17094e5..7549907 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -121,3 +121,11 @@ ignore = [
   "E203",  # Whitespace before ":". Not PEP8 compliant (https://github.com/psf/black/issues/315)
   "W503",  # https://peps.python.org/pep-0008/#should-a-line-break-before-or-after-a-binary-operator
 ]
+
+[tool.coverage.report]
+exclude_lines = [
+  "pragma: no cover",
+  "@overload",
+  "if TYPE_CHECKING:",
+  "if typing.TYPE_CHECKING:"
+]


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 36bba12..a3ae055 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2,8 +2,9 @@
 
 import unittest.mock as mock
 import pytest
-import era5cli.cli as cli
+import era5cli.args
 import era5cli.inputref as ref
+from era5cli import cli
 from era5cli import key_management
 
 
@@ -27,7 +28,7 @@ def test_parse_args():
     assert args.endyear == 2008
     assert args.ensemble
     assert args.format == "netcdf"
-    assert args.hours == list(range(0, 24))
+    assert args.hours == list(range(24))
     assert args.levels == ref.PLEVELS
     assert args.months == list(range(1, 13))
     assert args.outputprefix == "era5"
@@ -150,9 +151,9 @@ def test_period_args():
         "--ensemble",
     ]
     args = cli._parse_args(argv)
-    period_args = cli._set_period_args(args)
-    # Period_args consists of (synoptic, statistics, days, hours)
-    assert period_args == (None, None, None, [0])
+    period_args = era5cli.args.periods.set_period_args(args)
+    # Period_args consists of (synoptic, statistics, splitmonths, days, hours)
+    assert period_args == (None, None, False, None, [0])
 
     argv = [
         "monthly",
@@ -166,9 +167,9 @@ def test_period_args():
         "--ensemble",
     ]
     args = cli._parse_args(argv)
-    period_args = cli._set_period_args(args)
-    # Period_args consists of (synoptic, statistics, days, hours)
-    assert period_args == (True, None, None, [4, 7])
+    period_args = era5cli.args.periods.set_period_args(args)
+    # Period_args consists of (synoptic, statistics, splitmonths, days, hours)
+    assert period_args == (True, None, False, None, [4, 7])
 
     argv = [
         "monthly",
@@ -180,15 +181,15 @@ def test_period_args():
         "--ensemble",
     ]
     args = cli._parse_args(argv)
-    period_args = cli._set_period_args(args)
+    period_args = era5cli.args.periods.set_period_args(args)
     # Period_args consists of (synoptic, statistics, days, hours)
-    assert period_args == (True, None, None, range(0, 24))
+    assert period_args == (True, None, False, None, range(24))
 
-    # test whether the info option does not end up in _set_period_args
+    # test whether the info option does not end up in set_period_args
     argv = ["info", "2Dvars"]
     args = cli._parse_args(argv)
     with pytest.raises(AttributeError):
-        assert cli._set_period_args(args)
+        assert era5cli.args.periods.set_period_args(args)
 
 
 def test_level_arguments():
@@ -347,3 +348,39 @@ def test_config_invalid(mock_a, mock_b, capfd):
     cli._execute(args)
     out, _ = capfd.readouterr()
     assert "Error: the UID and key are rejected" in out
+
+
+class TestConfigControlFlow:
+    """Test the args.config.config_control_flow function."""
+
+    @mock.patch(
+        "era5cli.key_management.load_era5cli_config",
+        return_value=("https://www.test.org/", "123:abc-def"),
+    )
+    def test_config_show(self, mock, capsys):
+        args = cli._parse_args(["config", "--show"])
+        cli._execute(args)
+
+        expected = (
+            "Contents of .config/era5cli.txt:\n"
+            "    uid: 123\n"
+            "    key: abc-def\n"
+            "    url: https://www.test.org/\n"
+        )
+        out, _ = capsys.readouterr()
+        assert expected in out
+
+    @pytest.mark.parametrize(
+        "input_args",
+        [
+            ["config", "--show", "--uid", "123"],
+            ["config", "--show", "--key", "abc-def"],
+            ["config", "--show", "--uid", "123", "--key", "abc-def"],
+            ["config", "--key", "abc-def"],
+            ["config", "--uid", "123"],
+        ],
+    )
+    def test_config_inputerror(self, input_args):
+        with pytest.raises(era5cli.args.config.InputError):
+            args = cli._parse_args(input_args)
+            cli._execute(args)
diff --git a/tests/test_config.py b/tests/test_config.py
index 1b2a859..1b2cee1 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -31,6 +31,8 @@ def valid_path_cds(tmp_path_factory):
 
 
 class TestEra5CliConfig:
+    """Test the functionality when the /.config/era5cli.txt file exists."""
+
     def test_load_era5cli_config(self, valid_path_era5):
         with patch("era5cli.key_management.ERA5CLI_CONFIG_PATH", valid_path_era5):
             assert key_management.load_era5cli_config() == ("b", "123:abc-def")
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
index 1534e37..34b67f1 100644
--- a/tests/test_fetch.py
+++ b/tests/test_fetch.py
@@ -2,6 +2,7 @@
 
 import unittest.mock as mock
 import pytest
+from era5cli import _request_size
 from era5cli import fetch
 
 
@@ -39,6 +40,7 @@ def initialize(
     hours=list(range(24)),
     prelimbe=False,
     land=False,
+    splitmonths=False,
 ):
     with mock.patch(
         "era5cli.fetch.key_management.load_era5cli_config",
@@ -63,13 +65,14 @@ def initialize(
             threads=threads,
             prelimbe=prelimbe,
             land=land,
+            splitmonths=splitmonths,
         )
 
 
 @mock.patch(
     "era5cli.fetch.key_management.load_era5cli_config", return_value=("url", "key:uid")
 )
-def test_init(load_era5cli_config):
+def test_init(mockpatch):
     """Test init function of Fetch class."""
     era5 = fetch.Fetch(
         years=[2008, 2009],
@@ -83,7 +86,7 @@ def test_init(load_era5cli_config):
         ensemble=True,
         statistics=None,
         synoptic=None,
-        pressurelevels=None,
+        pressurelevels="surface",
         merge=False,
         threads=2,
         prelimbe=False,
@@ -100,7 +103,7 @@ def test_init(load_era5cli_config):
     assert era5.ensemble
     assert era5.statistics is None
     assert era5.synoptic is None
-    assert era5.pressure_levels is None
+    assert era5.pressure_levels == "surface"
     assert not era5.merge
     assert era5.threads == 2
     assert not era5.prelimbe
@@ -108,12 +111,24 @@ def test_init(load_era5cli_config):
 
     # initializing hourly variable with days=None should result in ValueError
     with pytest.raises(TypeError):
-        era5 = initialize(variables=["temperature"], period="hourly", days=None)
+        era5 = initialize(
+            variables=["temperature"], period="hourly", days=None, pressurelevels=[1]
+        )
 
     # initializing monthly variable with days=None returns fetch.Fetch object
-    era5 = initialize(variables=["temperature"], period="monthly", days=None)
+    era5 = initialize(
+        variables=["temperature"], period="monthly", days=None, pressurelevels=[1]
+    )
     assert isinstance(era5, fetch.Fetch)
 
+    with pytest.raises(_request_size.TooLargeRequestError):
+        initialize(
+            land=True, variables=["skin_temperature"], ensemble=False, splitmonths=False
+        )
+
+    with pytest.raises(ValueError, match="are not compatible"):
+        initialize(merge=True, splitmonths=True)
+
 
 @mock.patch("cdsapi.Client", autospec=True)
 @mock.patch("era5cli.utils.append_history", autospec=True)
@@ -153,15 +168,14 @@ def test_fetch_nodryrun(cds, era5cli_utilsappend_history):
     assert era5.fetch() is None
 
     # invalid pressure level should raise ValueError
-    era5 = initialize(
-        outputformat="grib",
-        merge=True,
-        threads=None,
-        pressurelevels=[1, 2, 9],
-        variables=["temperature"],
-    )
     with pytest.raises(ValueError):
-        assert era5.fetch()
+        era5 = initialize(
+            outputformat="grib",
+            merge=True,
+            threads=None,
+            pressurelevels=[1, 2, 9],
+            variables=["temperature"],
+        )
 
     # invalid variable name should raise ValueError
     era5 = initialize(
@@ -234,74 +248,61 @@ def test_define_outputfilename():
     fn = "era5_total_precipitation_2008-2009_hourly_ensemble_synoptic.grb"
     assert fname == fn
 
-    era5 = initialize(land=True, ensemble=False)
+    era5 = initialize(land=True, ensemble=False, splitmonths=True)
     era5._extension()
-    fname = era5._define_outputfilename("total_precipitation", [2008])
-    fn = "era5-land_total_precipitation_2008_hourly.nc"
+    fname = era5._define_outputfilename("total_precipitation", [2008], month="01")
+    fn = "era5-land_total_precipitation_2008-01_hourly.nc"
     assert fname == fn
 
     era5.area = [90.0, -180.0, -90.0, 180.0]
-    fname = era5._define_outputfilename("total_precipitation", [2008])
-    fn = "era5-land_total_precipitation_2008_hourly_180W-180E_90S-90N.nc"
+    fname = era5._define_outputfilename("total_precipitation", [2008], month="01")
+    fn = "era5-land_total_precipitation_2008-01_hourly_180W-180E_90S-90N.nc"
     assert fname == fn
 
     era5.area = [90.0, -180.0, -80.999, 170.001]
-    fname = era5._define_outputfilename("total_precipitation", [2008])
-    fn = "era5-land_total_precipitation_2008_hourly_180W-170E_81S-90N.nc"
+    fname = era5._define_outputfilename("total_precipitation", [2008], month="01")
+    fn = "era5-land_total_precipitation_2008-01_hourly_180W-170E_81S-90N.nc"
     assert fname == fn
 
     era5.area = [0, 120, -90, 180]
-    fname = era5._define_outputfilename("total_precipitation", [2008])
-    fn = "era5-land_total_precipitation_2008_hourly_120E-180E_90S-0N.nc"
+    fname = era5._define_outputfilename("total_precipitation", [2008], month="01")
+    fn = "era5-land_total_precipitation_2008-01_hourly_120E-180E_90S-0N.nc"
     assert fname == fn
 
 
-def test_number_outputfiles(capsys):
+_vars = ["total_precipitation", "runoff"]
+_years = [2007, 2008, 2009]
+
+
+@pytest.mark.parametrize(
+    "variables, years, merge, ensemble, splitmonths, expected",
+    [
+        (_vars, _years, False, False, False, 2 * 3),
+        (_vars, _years, True, False, False, 2),  # Test merge
+        (_vars, _years, False, True, False, 2 * 3),  # Current default
+        (_vars, _years, False, True, True, 2 * 3 * 12),  # Future default
+        (_vars, _years[:1], False, False, False, 2 * 1),
+        (_vars[:1], _years, False, False, False, 1 * 3),
+    ],
+)
+def test_number_outputfiles(
+    capsys, variables, years, merge, ensemble, splitmonths, expected
+):
     """Test function for the number of outputs."""
     # two variables and three years
     era5 = initialize(
-        variables=["total_precipitation", "runoff"],
-        years=[2007, 2008, 2009],
-        merge=False,
-    )
-    era5.fetch(dryrun=True)
-    captured = capsys.readouterr()
-    outputlength = len(captured.out.split("\n")) - 1
-    if not era5.merge:
-        # No. of outputs is 2*3 = 6 if merge = False
-        assert outputlength == len(era5.years) * len(era5.variables)
-    else:
-        # No. of outputs is 2*1 = 2 if merge = True
-        assert outputlength == len(era5.variables)
-
-    # one variable and three years
-    era5 = initialize(
-        variables=["total_precipitation"], years=[2007, 2008, 2009], merge=False
-    )
-    era5.fetch(dryrun=True)
-    captured = capsys.readouterr()
-    outputlength = len(captured.out.split("\n")) - 1
-    if not era5.merge:
-        # No. of outputs is 1*3 = 3 if merge = False
-        assert outputlength == len(era5.years) * len(era5.variables)
-    else:
-        # No. of outputs is 1 if merge = True
-        assert outputlength == len(era5.variables)
-
-    # two variables and one year
-    era5 = initialize(
-        variables=["total_precipitation", "runoff"], years=[2007], merge=False
+        variables=variables,
+        years=years,
+        merge=merge,
+        ensemble=ensemble,
+        splitmonths=splitmonths,
     )
     era5.fetch(dryrun=True)
     captured = capsys.readouterr()
-    outputlength = len(captured.out.split("\n")) - 1
-    if not era5.merge:
-        # No. of outputs is 2*1 = 2 if merge = False
-        assert outputlength == len(era5.years) * len(era5.variables)
-    else:
-        # No. of outputs is 2 if merge = True
-        assert outputlength == len(era5.variables)
-    del era5, captured
+
+    # Every request has 'product_type' in it once, ie. the number of requests equals:
+    nrequests = captured.out.count("product_type")
+    assert nrequests == expected
 
 
 def test_product_type():
@@ -558,9 +559,8 @@ def test_build_request():
     assert request == req
 
     # requesting 3d variable with pressurelevels=None should give a ValueError
-    era5 = initialize(variables=["temperature"], pressurelevels=None)
     with pytest.raises(ValueError):
-        assert era5._build_request("temperature", [2008])
+        era5 = initialize(variables=["temperature"], pressurelevels=None)
 
     # requesting data from orography should call geopotential
     era5 = initialize()
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 0dfcbdb..fb05754 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -57,16 +57,16 @@ call_result = [
     },
     {
         # without --levels surface, geopotential calls pressure level data
+        # Note: only request a single month to avoid TooLargeRequest
         "call": dedent(
             """\
-            era5cli hourly --variables geopotential --startyear 2008
+            era5cli hourly --variables geopotential --startyear 2008 --months 01
             --dryrun"""
         ),
         "result": dedent(
             """\
             reanalysis-era5-pressure-levels {'variable': 'geopotential',
-            'year': 2008, 'month': ['01', '02', '03', '04', '05', '06', '07',
-            '08', '09', '10', '11', '12'], 'time': ['00:00', '01:00', '02:00',
+            'year': 2008, 'month': ['01'], 'time': ['00:00', '01:00', '02:00',
             '03:00', '04:00', '05:00', '06:00', '07:00', '08:00', '09:00',
             '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00',
             '17:00', '18:00', '19:00', '20:00', '21:00', '22:00', '23:00'],
@@ -143,7 +143,7 @@ def test_main(call_result, capsys, caplog):
         ):
             main(call)
     captured = capsys.readouterr().out
-    assert result == captured
+    assert result in captured
     try:
         warn = call_result["warn"]
         assert warn in caplog.text