From 9c12acc29061af3aa16090319fae984711857d4a Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:04:30 +0200 Subject: [PATCH 1/8] add expected_end_date for time chunking. --- xarray_ecmwf/client_cdsapi.py | 6 +++- xarray_ecmwf/client_common.py | 61 ++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/xarray_ecmwf/client_cdsapi.py b/xarray_ecmwf/client_cdsapi.py index db56c4c..e6819ed 100644 --- a/xarray_ecmwf/client_cdsapi.py +++ b/xarray_ecmwf/client_cdsapi.py @@ -52,6 +52,7 @@ class CdsapiRequestChunker: merge_date_time: bool = True time_dim: str = "time" time_sep: str = "/" + expected_end_date: str | None = None def get_request_dimensions(self) -> dict[str, list[Any]]: request_dimensions: dict[str, list[Any]] = {} @@ -116,7 +117,10 @@ def compute_chunked_request_coords(self) -> dict[str, Any]: time_chunk, time_chunk_requests, ) = client_common.build_time_chunk_requests( - self.request | override_time, self.request_chunks, self.time_sep + self.request | override_time, + self.request_chunks, + self.time_sep, + expected_end_date=self.expected_end_date, ) if len(time_chunk_requests) > 1: self.chunks[self.time_dim] = time_chunk diff --git a/xarray_ecmwf/client_common.py b/xarray_ecmwf/client_common.py index abb3b72..67f5d55 100644 --- a/xarray_ecmwf/client_common.py +++ b/xarray_ecmwf/client_common.py @@ -263,8 +263,63 @@ def build_chunk_ymd_year_requests( return np.array(datetimes), tuple(chunks), chunk_requests +def trim_ymd_time_to_expected_end_date( + time: np.typing.NDArray[np.datetime64], + time_chunk: int | tuple[int, ...], + time_chunk_requests: list[tuple[int, dict[str, Any]]], + expected_end_date: str, +) -> tuple[ + np.typing.NDArray[np.datetime64], + int | tuple[int, ...], + list[tuple[int, dict[str, Any]]], +]: + """Cut a year/month/day time coordinate at ``expected_end_date``. + + ``build_chunk_ymd_requests`` builds every timestamp in the cartesian + product, so the last year is a full set of months even when CDS has not + published them yet. Timestamps after the cutoff are dropped, and the + chunk sizes are resized so they still add up to the coordinate that + remains. The cutoff is inclusive: a timestamp equal to + ``expected_end_date`` stays. A chunk that lies entirely past the cutoff + is removed. A chunk that crosses it keeps the same CDS request and only + its length changes. + """ + if len(time) == 0: + raise ValueError("cannot apply expected_end_date to an empty time coordinate") + + end = pd.Timestamp(expected_end_date).to_datetime64().astype("datetime64[ns]") + cut = int(np.searchsorted(time, end, side="right")) + if cut == 0: + raise ValueError( + f"expected_end_date {expected_end_date} is before the first " + f"timestamp {time[0]}" + ) + if cut >= len(time): + return time, time_chunk, time_chunk_requests + + starts = [start for start, _ in time_chunk_requests] + new_requests: list[tuple[int, dict[str, Any]]] = [] + new_sizes: list[int] = [] + for index, (start, chunk_request) in enumerate(time_chunk_requests): + if start >= cut: + break + next_start = starts[index + 1] if index + 1 < len(starts) else len(time) + new_sizes.append(min(next_start, cut) - start) + new_requests.append((start, chunk_request)) + + trimmed = time[:cut] + if isinstance(time_chunk, int) and all(size == time_chunk for size in new_sizes): + return trimmed, time_chunk, new_requests + if isinstance(time_chunk, int) and len(new_sizes) == 1: + return trimmed, new_sizes[0], new_requests + return trimmed, tuple(new_sizes), new_requests + + def build_time_chunk_requests( - request: dict[str, Any], request_chunks: dict[str, int], sep: str = "/" + request: dict[str, Any], + request_chunks: dict[str, int], + sep: str = "/", + expected_end_date: str | None = None, ) -> tuple[ np.typing.NDArray[np.datetime64], int | tuple[int, ...], @@ -274,6 +329,10 @@ def build_time_chunk_requests( time, time_chunk, time_chunk_requests = build_chunk_ymd_requests( request, request_chunks ) + if expected_end_date is not None: + time, time_chunk, time_chunk_requests = trim_ymd_time_to_expected_end_date( + time, time_chunk, time_chunk_requests, expected_end_date + ) elif "date" in request: time, time_chunk, time_chunk_requests = build_chunk_date_requests( request, request_chunks, sep From 2526f67a47a01243fb3165baebbd06cd0a53d1a6 Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:20:52 +0200 Subject: [PATCH 2/8] Add unit tests for several cases of new function utilization. --- tests/test_10_client_common.py | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/test_10_client_common.py b/tests/test_10_client_common.py index 07c80b7..3969927 100644 --- a/tests/test_10_client_common.py +++ b/tests/test_10_client_common.py @@ -2,6 +2,7 @@ import itertools from typing import Any +import numpy as np import pandas as pd import pytest @@ -188,6 +189,123 @@ def test_build_chunk_ymd_year_requests_groups_years( assert time_chunk == tuple(len(group) for group in expected_year_groups) +def _monthly_year_request() -> dict[str, Any]: + return { + "year": ["2024", "2025", "2026"], + "month": [f"{month:02}" for month in range(1, 13)], + "day": ["01"], + "time": ["00:00"], + } + + +def test_expected_end_date_trims_partial_last_year() -> None: + time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( + _monthly_year_request(), + {"year": 1}, + expected_end_date="2026-07-01", + ) + assert len(time) == 12 + 12 + 7 + assert time[-1] == np.datetime64("2026-07-01T00:00", "ns") + assert time_chunk == (12, 12, 7) + assert [chunk_request["year"] for _, chunk_request in time_chunk_requests] == [ + ["2024"], + ["2025"], + ["2026"], + ] + + +def test_expected_end_date_drops_years_after_the_cutoff() -> None: + time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( + _monthly_year_request(), + {"year": 1}, + expected_end_date="2025-12-01", + ) + assert len(time) == 24 + assert time[-1] == np.datetime64("2025-12-01T00:00", "ns") + assert time_chunk == (12, 12) + assert [chunk_request["year"] for _, chunk_request in time_chunk_requests] == [ + ["2024"], + ["2025"], + ] + + +def test_expected_end_date_shrinks_a_multi_year_chunk() -> None: + time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( + _monthly_year_request(), + {"year": 2}, + expected_end_date="2025-06-01", + ) + assert len(time) == 12 + 6 + assert time[-1] == np.datetime64("2025-06-01T00:00", "ns") + assert time_chunk == (18,) + assert time_chunk_requests == [(0, {"year": ["2024", "2025"]})] + + +def test_expected_end_date_on_a_chunk_boundary_keeps_equal_day_chunks() -> None: + request = { + "year": ["2024"], + "month": ["01", "02"], + "day": ["01", "02"], + "time": ["00:00"], + } + time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( + request, + {"day": 1}, + expected_end_date="2024-01-02", + ) + assert list(time) == [ + np.datetime64("2024-01-01T00:00", "ns"), + np.datetime64("2024-01-02T00:00", "ns"), + ] + assert time_chunk == 1 + assert len(time_chunk_requests) == 2 + + +def test_expected_end_date_shrinks_the_last_equal_sized_chunk() -> None: + request = { + "year": ["2024"], + "month": ["01"], + "day": ["01", "02"], + "time": ["00:00", "12:00"], + } + time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( + request, + {"day": 1}, + expected_end_date="2024-01-02T00:00", + ) + assert list(time) == [ + np.datetime64("2024-01-01T00:00", "ns"), + np.datetime64("2024-01-01T12:00", "ns"), + np.datetime64("2024-01-02T00:00", "ns"), + ] + assert time_chunk == (2, 1) + assert [chunk_request["day"] for _, chunk_request in time_chunk_requests] == [ + "01", + "02", + ] + + +def test_expected_end_date_past_the_last_timestamp_is_a_no_op() -> None: + full, full_chunk, full_requests = client_common.build_time_chunk_requests( + _monthly_year_request(), {"year": 1} + ) + trimmed, trimmed_chunk, trimmed_requests = client_common.build_time_chunk_requests( + _monthly_year_request(), {"year": 1}, expected_end_date="2027-01-01" + ) + assert list(trimmed) == list(full) + assert trimmed_chunk == full_chunk + assert trimmed_requests == full_requests + + +def test_expected_end_date_before_the_first_timestamp_raises() -> None: + with pytest.raises(ValueError, match="before the first timestamp"): + client_common.build_time_chunk_requests( + _monthly_year_request(), + {"year": 1}, + expected_end_date="2020-01-01", + ) + + def test_build_chunk_ymd_year_requests_invalid() -> None: request = { "year": ["2022"], From 0b4fd2da1171433e9e29bdff3e0d6730893b4147 Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:21:46 +0200 Subject: [PATCH 3/8] update the readme to include the new year chunking request functionality. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2489a90..9d6a445 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ xarray-ecmwf is a Python library and [Xarray](https://docs.xarray.dev) backend w - the [ECMWF Open data](https://www.ecmwf.int/en/forecasts/datasets/open-data) via [ecmwf-opendata](https://github.com/ecmwf/ecmwf-opendata): High resolution forecasts, ensemble forecast - allows lazy loading the data and well integrated with [Dask](https://www.dask.org) and [Dask.distributed](https://distributed.dask.org) - allows chunking the input request according to a configurable splitting strategy. Allowed strategies: + - by N years - by one month - by one day - supports requests returning a single GRIB file, via [cfgrib](https://github.com/ecmwf/cfgrib) From 2486332955901c6825425fb7174f4b211c9b8cf7 Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:49:19 +0200 Subject: [PATCH 4/8] rename expected_end_date into force_end_date. --- tests/test_10_client_common.py | 28 ++++++++++++++-------------- xarray_ecmwf/client_cdsapi.py | 4 ++-- xarray_ecmwf/client_common.py | 23 +++++++++++------------ 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/tests/test_10_client_common.py b/tests/test_10_client_common.py index 3969927..a0443db 100644 --- a/tests/test_10_client_common.py +++ b/tests/test_10_client_common.py @@ -198,11 +198,11 @@ def _monthly_year_request() -> dict[str, Any]: } -def test_expected_end_date_trims_partial_last_year() -> None: +def test_force_end_date_trims_partial_last_year() -> None: time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( _monthly_year_request(), {"year": 1}, - expected_end_date="2026-07-01", + force_end_date="2026-07-01", ) assert len(time) == 12 + 12 + 7 assert time[-1] == np.datetime64("2026-07-01T00:00", "ns") @@ -214,11 +214,11 @@ def test_expected_end_date_trims_partial_last_year() -> None: ] -def test_expected_end_date_drops_years_after_the_cutoff() -> None: +def test_force_end_date_drops_years_after_the_cutoff() -> None: time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( _monthly_year_request(), {"year": 1}, - expected_end_date="2025-12-01", + force_end_date="2025-12-01", ) assert len(time) == 24 assert time[-1] == np.datetime64("2025-12-01T00:00", "ns") @@ -229,11 +229,11 @@ def test_expected_end_date_drops_years_after_the_cutoff() -> None: ] -def test_expected_end_date_shrinks_a_multi_year_chunk() -> None: +def test_force_end_date_shrinks_a_multi_year_chunk() -> None: time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( _monthly_year_request(), {"year": 2}, - expected_end_date="2025-06-01", + force_end_date="2025-06-01", ) assert len(time) == 12 + 6 assert time[-1] == np.datetime64("2025-06-01T00:00", "ns") @@ -241,7 +241,7 @@ def test_expected_end_date_shrinks_a_multi_year_chunk() -> None: assert time_chunk_requests == [(0, {"year": ["2024", "2025"]})] -def test_expected_end_date_on_a_chunk_boundary_keeps_equal_day_chunks() -> None: +def test_force_end_date_on_a_chunk_boundary_keeps_equal_day_chunks() -> None: request = { "year": ["2024"], "month": ["01", "02"], @@ -251,7 +251,7 @@ def test_expected_end_date_on_a_chunk_boundary_keeps_equal_day_chunks() -> None: time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( request, {"day": 1}, - expected_end_date="2024-01-02", + force_end_date="2024-01-02", ) assert list(time) == [ np.datetime64("2024-01-01T00:00", "ns"), @@ -261,7 +261,7 @@ def test_expected_end_date_on_a_chunk_boundary_keeps_equal_day_chunks() -> None: assert len(time_chunk_requests) == 2 -def test_expected_end_date_shrinks_the_last_equal_sized_chunk() -> None: +def test_force_end_date_shrinks_the_last_equal_sized_chunk() -> None: request = { "year": ["2024"], "month": ["01"], @@ -271,7 +271,7 @@ def test_expected_end_date_shrinks_the_last_equal_sized_chunk() -> None: time, time_chunk, time_chunk_requests = client_common.build_time_chunk_requests( request, {"day": 1}, - expected_end_date="2024-01-02T00:00", + force_end_date="2024-01-02T00:00", ) assert list(time) == [ np.datetime64("2024-01-01T00:00", "ns"), @@ -285,24 +285,24 @@ def test_expected_end_date_shrinks_the_last_equal_sized_chunk() -> None: ] -def test_expected_end_date_past_the_last_timestamp_is_a_no_op() -> None: +def test_force_end_date_past_the_last_timestamp_is_a_no_op() -> None: full, full_chunk, full_requests = client_common.build_time_chunk_requests( _monthly_year_request(), {"year": 1} ) trimmed, trimmed_chunk, trimmed_requests = client_common.build_time_chunk_requests( - _monthly_year_request(), {"year": 1}, expected_end_date="2027-01-01" + _monthly_year_request(), {"year": 1}, force_end_date="2027-01-01" ) assert list(trimmed) == list(full) assert trimmed_chunk == full_chunk assert trimmed_requests == full_requests -def test_expected_end_date_before_the_first_timestamp_raises() -> None: +def test_force_end_date_before_the_first_timestamp_raises() -> None: with pytest.raises(ValueError, match="before the first timestamp"): client_common.build_time_chunk_requests( _monthly_year_request(), {"year": 1}, - expected_end_date="2020-01-01", + force_end_date="2020-01-01", ) diff --git a/xarray_ecmwf/client_cdsapi.py b/xarray_ecmwf/client_cdsapi.py index e6819ed..0c48688 100644 --- a/xarray_ecmwf/client_cdsapi.py +++ b/xarray_ecmwf/client_cdsapi.py @@ -52,7 +52,7 @@ class CdsapiRequestChunker: merge_date_time: bool = True time_dim: str = "time" time_sep: str = "/" - expected_end_date: str | None = None + force_end_date: str | None = None def get_request_dimensions(self) -> dict[str, list[Any]]: request_dimensions: dict[str, list[Any]] = {} @@ -120,7 +120,7 @@ def compute_chunked_request_coords(self) -> dict[str, Any]: self.request | override_time, self.request_chunks, self.time_sep, - expected_end_date=self.expected_end_date, + force_end_date=self.force_end_date, ) if len(time_chunk_requests) > 1: self.chunks[self.time_dim] = time_chunk diff --git a/xarray_ecmwf/client_common.py b/xarray_ecmwf/client_common.py index 67f5d55..ef5ba0a 100644 --- a/xarray_ecmwf/client_common.py +++ b/xarray_ecmwf/client_common.py @@ -263,36 +263,35 @@ def build_chunk_ymd_year_requests( return np.array(datetimes), tuple(chunks), chunk_requests -def trim_ymd_time_to_expected_end_date( +def trim_ymd_time_to_force_end_date( time: np.typing.NDArray[np.datetime64], time_chunk: int | tuple[int, ...], time_chunk_requests: list[tuple[int, dict[str, Any]]], - expected_end_date: str, + force_end_date: str, ) -> tuple[ np.typing.NDArray[np.datetime64], int | tuple[int, ...], list[tuple[int, dict[str, Any]]], ]: - """Cut a year/month/day time coordinate at ``expected_end_date``. + """Cut a year/month/day time coordinate at ``force_end_date``. ``build_chunk_ymd_requests`` builds every timestamp in the cartesian product, so the last year is a full set of months even when CDS has not published them yet. Timestamps after the cutoff are dropped, and the chunk sizes are resized so they still add up to the coordinate that remains. The cutoff is inclusive: a timestamp equal to - ``expected_end_date`` stays. A chunk that lies entirely past the cutoff + ``force_end_date`` stays. A chunk that lies entirely past the cutoff is removed. A chunk that crosses it keeps the same CDS request and only its length changes. """ if len(time) == 0: - raise ValueError("cannot apply expected_end_date to an empty time coordinate") + raise ValueError("cannot apply force_end_date to an empty time coordinate") - end = pd.Timestamp(expected_end_date).to_datetime64().astype("datetime64[ns]") + end = pd.Timestamp(force_end_date).to_datetime64().astype("datetime64[ns]") cut = int(np.searchsorted(time, end, side="right")) if cut == 0: raise ValueError( - f"expected_end_date {expected_end_date} is before the first " - f"timestamp {time[0]}" + f"end_date {force_end_date} is before the first timestamp {time[0]}" ) if cut >= len(time): return time, time_chunk, time_chunk_requests @@ -319,7 +318,7 @@ def build_time_chunk_requests( request: dict[str, Any], request_chunks: dict[str, int], sep: str = "/", - expected_end_date: str | None = None, + force_end_date: str | None = None, ) -> tuple[ np.typing.NDArray[np.datetime64], int | tuple[int, ...], @@ -329,9 +328,9 @@ def build_time_chunk_requests( time, time_chunk, time_chunk_requests = build_chunk_ymd_requests( request, request_chunks ) - if expected_end_date is not None: - time, time_chunk, time_chunk_requests = trim_ymd_time_to_expected_end_date( - time, time_chunk, time_chunk_requests, expected_end_date + if force_end_date is not None: + time, time_chunk, time_chunk_requests = trim_ymd_time_to_force_end_date( + time, time_chunk, time_chunk_requests, force_end_date ) elif "date" in request: time, time_chunk, time_chunk_requests = build_chunk_date_requests( From 8b98c2034cac8d0d5d8afc89039355ef4b7b57d1 Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:23:31 +0200 Subject: [PATCH 5/8] Adds Logger to client_common.py --- xarray_ecmwf/client_common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xarray_ecmwf/client_common.py b/xarray_ecmwf/client_common.py index ef5ba0a..93239cc 100644 --- a/xarray_ecmwf/client_common.py +++ b/xarray_ecmwf/client_common.py @@ -1,10 +1,13 @@ import calendar +import logging from typing import Any, ContextManager, Protocol import numpy as np import pandas as pd import xarray as xr +LOGGER = logging.getLogger(__name__) + class RequestClientProtocol(Protocol): def __init__(self, client_kwargs: dict[str, Any]) -> None: ... @@ -336,6 +339,9 @@ def build_time_chunk_requests( time, time_chunk, time_chunk_requests = build_chunk_date_requests( request, request_chunks, sep ) + if force_end_date is not None: + msg = "forced_end_date param is ignored for date-interval requests." + LOGGER.warning(msg) else: raise ValueError("request must contain either 'year' or 'date'") From 372a9e0f087e2a56136f85310a5302b38b4a96d9 Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:25:21 +0200 Subject: [PATCH 6/8] Ensure force_end_date is used even if there's only one chunk downloaded as sample. --- xarray_ecmwf/client_cdsapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray_ecmwf/client_cdsapi.py b/xarray_ecmwf/client_cdsapi.py index 0c48688..aa50055 100644 --- a/xarray_ecmwf/client_cdsapi.py +++ b/xarray_ecmwf/client_cdsapi.py @@ -122,7 +122,7 @@ def compute_chunked_request_coords(self) -> dict[str, Any]: self.time_sep, force_end_date=self.force_end_date, ) - if len(time_chunk_requests) > 1: + if self.force_end_date is not None or len(time_chunk_requests) > 1: self.chunks[self.time_dim] = time_chunk self.chunk_requests[self.time_dim] = time_chunk_requests self.chunked_coords[self.time_dim] = xr.IndexVariable( # type: ignore From eadb54a274167c9d4316320d814ee1497bd02a76 Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:41:57 +0200 Subject: [PATCH 7/8] Devise new UT to test wether the correct time dimension is passed by the Chunker. --- tests/test_10_client_common.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_10_client_common.py b/tests/test_10_client_common.py index a0443db..a19cadc 100644 --- a/tests/test_10_client_common.py +++ b/tests/test_10_client_common.py @@ -6,7 +6,7 @@ import pandas as pd import pytest -from xarray_ecmwf import client_common +from xarray_ecmwf import client_cdsapi, client_common ALL_MONTHS = [f"{m:02}" for m in range(1, 13)] ALL_DAYS = [f"{d:02}" for d in range(1, 32)] @@ -297,6 +297,24 @@ def test_force_end_date_past_the_last_timestamp_is_a_no_op() -> None: assert trimmed_requests == full_requests +def test_force_end_date_stays_when_one_chunk_remains() -> None: + chunker = client_cdsapi.CdsapiRequestChunker( + request={ + "year": ["1950", "1951"], + "month": [f"{month:02}" for month in range(1, 13)], + "day": ["01"], + "time": ["00:00"], + }, + request_chunks={"year": 1}, + time_dim="valid_time", + force_end_date="1950-08-17", + ) + valid_time = chunker.compute_chunked_request_coords()["valid_time"] + assert valid_time.size == 8 + assert valid_time.values[-1] == np.datetime64("1950-08-01T00:00", "ns") + assert chunker.first_chunk_request()["year"] == ["1950"] + + def test_force_end_date_before_the_first_timestamp_raises() -> None: with pytest.raises(ValueError, match="before the first timestamp"): client_common.build_time_chunk_requests( From 8ace79fca1a39969f62a79e8279d1db6958e0bf8 Mon Sep 17 00:00:00 2001 From: GianmarcoLattaruolo <87989501+GianmarcoLattaruolo@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:42:53 +0200 Subject: [PATCH 8/8] Document force_end_date in the readme. --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 9d6a445..9c67cc4 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,25 @@ Attributes: ``` +## `force_end_date` + +Pass `force_end_date` in `request_chunker_kwargs`. The keyword uses underscores. + +For a request built from `year`, `month`, `day`, and `time`, the time coordinate is every combination of those lists. Timestamps after `force_end_date` are dropped. A timestamp equal to it stays. The dataset coordinate ends at the last remaining timestamp, also when only one chunk is left. With a monthly series on day `01` and `force_end_date` `1950-08-17`, the last time is `1950-08-01`. + +```python +>>> ds = xr.open_dataset( +... request, +... engine="ecmwf", +... request_chunks={"year": 1}, +... request_chunker_kwargs={"force_end_date": "1950-08-17"}, +... ) +``` + +The option applies to requests that contain `year`. A request that uses `date` is left unchanged. + +The CDS request for a year still carries the original `month` list. The coordinate ends at the last timestamp that is not after `force_end_date`. + ## License ```