diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..e43012d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,34 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cec1d1..243b284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.1.4 - 2024-10-21 + +* Add `--skip` options to `earthkit-dateseq previous/next` + ## 0.1.3 - 2024-07-19 * Add `Sequence.nearest` and `earthkit-dateseq nearest` diff --git a/README.md b/README.md index a2162d6..3d6c458 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ # earthkit.time -:warning: This project is BETA and will be experimental for the foreseeable -future. Interfaces and functionality are likely to change, and the project -itself may be scrapped. DO NOT use this software in any project/software that is -operational. +:warning: This project is in the **BETA** stage of development. Please be aware +that interfaces and functionality may change as the project develops. If this +software is to be used in operational systems you are **strongly advised to use +a released tag in your system configuration**, and you should be willing to +accept incoming changes and bug fixes that require adaptations on your part. +ECMWF **does use** this software in operations and abides by the same caveats. Date and time manipulation routines for the use of weather data +## Documentation + +The documentation can be found at https://earthkit-time.readthedocs.io. + ## Python API ### When is the next Tuesday? diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api/calendar.rst b/docs/api/calendar.rst new file mode 100644 index 0000000..fa583dc --- /dev/null +++ b/docs/api/calendar.rst @@ -0,0 +1,6 @@ +earthkit.time.calendar - Calendar-related utilities +=================================================== + +.. automodule:: earthkit.time.calendar + :members: + :undoc-members: diff --git a/docs/api/climatology.rst b/docs/api/climatology.rst new file mode 100644 index 0000000..65261fb --- /dev/null +++ b/docs/api/climatology.rst @@ -0,0 +1,5 @@ +earthkit.time.climatology - Generate dates to compute model climates +==================================================================== + +.. automodule:: earthkit.time.climatology + :members: diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..35ac74d --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,10 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + sequence + climatology + calendar diff --git a/docs/api/sequence.rst b/docs/api/sequence.rst new file mode 100644 index 0000000..0b7028c --- /dev/null +++ b/docs/api/sequence.rst @@ -0,0 +1,25 @@ +earthkit.time.sequence - Manipulate sequence of dates +===================================================== + +.. module:: earthkit.time.sequence + +Sequence API +------------ + +Sequences are described by subclasses of :class:`Sequence`. + +.. autoclass:: Sequence + :members: + + +Built-in Sequences +------------------ + +.. autoclass:: DailySequence + +.. autoclass:: WeeklySequence + +.. autoclass:: MonthlySequence + +.. autoclass:: YearlySequence + diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..66efc0f --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,2 @@ +```{include} ../CHANGELOG.md +``` diff --git a/docs/cli/climdates.rst b/docs/cli/climdates.rst new file mode 100644 index 0000000..2166da0 --- /dev/null +++ b/docs/cli/climdates.rst @@ -0,0 +1,88 @@ +earthkit-climdates - Compute model climate dates +================================================ + +.. program:: earthkit-climdates + +The :program:`earthkit-climdates` can be invoked with various actions documented +here, as follows:: + + earthkit-climdates [args...] + +.. option:: -h, --help + + Print a help message and exit. Can be used as ``earthkit-climdates --help`` + as well as for actions, e.g. ``earthkit-climdates range --help``. + + +.. _cli_mclim_range: + +``range`` - Compute climatological date ranges +---------------------------------------------- + +Compute climatological date ranges, one day per year in a given range:: + + earthkit-climdates range [--sep ] (--from-date | --from-year ) (--to-date | --to-year ) + +The list is printed using the given separator, as documented in :ref:`cli_sep`. + +.. option:: --from-date + + Return dates starting from this one + +.. option:: --from-year + + Return dates starting from this year + +.. option:: --to-date + + Return dates up to this one + +.. option:: --to-year + + Return dates up to this year + +.. option:: date + + The date to use as a reference (YYYYMMDD) + + +``mclim`` - Compute sets of dates for model climatologies +--------------------------------------------------------- + +This combines a climatological range (see :ref:`cli_mclim_range`) and a +recurring source (e.g. twice a week). + +Usage:: + + earthkit-climdates mclim [--sep ] (--from-date | --from-year ) (--to-date | --to-year ) --before --after + +The sequence is described as documented in :ref:`cli_seq`. The list is printed +using the given separator, as documented in :ref:`cli_sep`. + +.. option:: --from-date + + Return dates starting from this one + +.. option:: --from-year + + Return dates starting from this year + +.. option:: --to-date + + Return dates up to this one + +.. option:: --to-year + + Return dates up to this year + +.. option:: --before + + Pick up all inputs starting from *num* days before the chosen date + +.. option:: --after + + Pick up all inputs up to *num* days after the chosen date + +.. option:: date + + The date to use as a reference (YYYYMMDD) diff --git a/docs/cli/date.rst b/docs/cli/date.rst new file mode 100644 index 0000000..116a634 --- /dev/null +++ b/docs/cli/date.rst @@ -0,0 +1,46 @@ +earthkit-date - Manipulate dates +================================ + +.. program:: earthkit-date + +The :program:`earthkit-date` can be invoked with various actions documented +here, as follows:: + + earthkit-date [args...] + +.. option:: -h, --help + + Print a help message and exit. Can be used as ``earthkit-date --help`` as well as + for actions, e.g. ``earthkit-date shift --help``. + + +``shift`` - shift a date +------------------------ + +Shift a date by the given number of days:: + + earthkit-date shift + +.. option:: date + + Reference date + +.. option:: days + + Number of days (can be negative) + + +``diff`` - subtract two dates +----------------------------- + +Subtract date2 from date1, returning the number of days:: + + earthkit-date diff + +.. option:: date1 + + First date (+) + +.. option:: date2 + + Second date (-) diff --git a/docs/cli/dateseq.rst b/docs/cli/dateseq.rst new file mode 100644 index 0000000..00d9288 --- /dev/null +++ b/docs/cli/dateseq.rst @@ -0,0 +1,177 @@ +earthkit-dateseq - Manipulate sequences of dates +================================================ + +.. program:: earthkit-dateseq + +The :program:`earthkit-dateseq` can be invoked with various actions documented +here, as follows:: + + earthkit-dateseq [args...] + +.. option:: -h, --help + + Print a help message and exit. Can be used as ``earthkit-dateseq --help`` as + well as for actions, e.g. ``earthkit-dateseq next --help``. + + +.. _cli_seq: + +Specifying sequences +-------------------- + +Sequences can be described according to their type, using the corresponding argument. + +.. option:: --daily + + Daily inputs + +.. option:: --weekly + + Weekly inputs on these days (slash-separated). Week days can be specified + either by number (0 = Monday, 1 = Tuesday, etc) or by any unambiguous prefix + of the name (case-insensitive, e.g. M, tue, Friday) + +.. option:: --monthly + + Monthly inputs on these days (slash-separated) + +.. option:: --yearly + + Yearly inputs on these days (MMDD, slash-separated) + +.. option:: --preset + + Name of a preset sequence, or path to a valid YAML preset file. Sequence + presets can be stored in the package as well as externally defined. If a + preset name is given, the corresponding file will be searched in + :envvar:`EARTHKIT_TIME_SEQ_PATH`, then in the package itself. + +.. envvar:: EARTHKIT_TIME_SEQ_PATH + + Colon-separated list of paths where to look for sequence presets. Will take + precedence over the ones provided by earthkit-time. + +.. option:: --excludes + + Exclude specific days from the sequence, as follows: + + * daily: exclude specific days of the month + * monthly: exclude specific dates in the year (MMDD) + * yearly: exclude specific dates (YYYYMMDD) + + +``previous``, ``next`` - Compute the previous and next date in the given sequence +--------------------------------------------------------------------------------- + +Usage:: + + earthkit-dateseq previous [--inclusive] [--skip ] + earthkit-dateseq next [--inclusive] [--skip ] + +The sequence is described as documented in :ref:`cli_seq`. + +.. option:: --inclusive + + If this flag is set and the given date is in the sequence, it is returned. + +.. option:: --skip + + If set, skip over the given number of dates. If :option:`--inclusive` is not + set and the date is in the sequence, it is skipped on top of that. + +.. option:: date + + The date to use as a reference (YYYYMMDD) + + +``nearest`` - Compute the nearest date in the given sequence +------------------------------------------------------------ + +Usage:: + + earthkit-dateseq nearest [--resolve ] + +The sequence is described as documented in :ref:`cli_seq`. + +.. option:: --resolve + + Can be either ``previous`` or ``next``. If two consecutive dates in the + sequence are equally close, use this one. By default, the previous date is + used. + +.. option:: date + + The date to use as a reference (YYYYMMDD) + + +``range`` - Compute the sequence dates that fall within a range +--------------------------------------------------------------- + +Usage:: + + earthkit-dateseq range [--sep ] [--exclude-start] [--exclude-end] + +The sequence is described as documented in :ref:`cli_seq`. The list is printed +using the given separator, as documented in :ref:`cli_sep`. + +.. option:: --exclude-start + + If specified and the start date is in the sequence, do not print it. + +.. option:: --exclude-end + + If specified and the end date is in the sequence, do not print it. + +.. option:: from + + Start date + +.. option:: to + + End date + + +``bracket`` - Compute the sequence dates around a date +------------------------------------------------------ + +Usage:: + + earthkit-dateseq bracket [--sep ] [--inclusive] + +The sequence is described as documented in :ref:`cli_seq`. The list is printed +using the given separator, as documented in :ref:`cli_sep`. + +.. option:: --inclusive + + If this flag is set and the given date is in the sequence, it is returned (not counted). + +.. option:: date + + The date to use as a reference (YYYYMMDD) + +.. option:: before + + Number of dates to print before the given date (default 1) + +.. option:: after + + Number of dates to print after the given date (default: same number as before) + + +.. _cli_sep: + +Formatting lists of dates +------------------------- + +If the following option is not set, each date will be printed on a separate +line. + +.. option:: --sep + + separators can be any string of characters, with some escape + sequences evaluated: + + * ``\0``, ``\a``, ``\b``, ``\f``, ``\n``, ``\r``, ``\t``, ``\v``: NUL, BEL, BS, FF, LF, CR, TAB, VT + * ``\xhh``: character with hex value ``hh`` + * ``\ooo``: character with octal value ``ooo`` + * ``\\``: literal ``\`` diff --git a/docs/cli/index.rst b/docs/cli/index.rst new file mode 100644 index 0000000..fec966c --- /dev/null +++ b/docs/cli/index.rst @@ -0,0 +1,12 @@ +.. _cli_ref: + +Command-line Interface +====================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + date + dateseq + climdates diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..634db07 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,54 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import datetime + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "earthkit-time" +author = "European Centre for Medium Range Weather Forecasts" + +year = datetime.datetime.now().year +years = "2024-%s" % (year,) +copyright = "%s, European Centre for Medium-Range Weather Forecasts (ECMWF)" % (years,) + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx_rtd_theme", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + + +# -- Options for autodoc ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration + +autodoc_member_order = "bysource" + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] + + +# -- Options for InterSphinx ------------------------------------------------- + +# Links to the additional documentation sites +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} diff --git a/docs/guide/api.rst b/docs/guide/api.rst new file mode 100644 index 0000000..d1469c9 --- /dev/null +++ b/docs/guide/api.rst @@ -0,0 +1,155 @@ +Python API +========== + +Manipulating sequences of dates +------------------------------- + +Sequences of dates are represented by the +:class:`~earthkit.time.sequence.Sequence` class. Specific types of sequence are +defined as subclasses: + +* :class:`~earthkit.time.sequence.DailySequence` for daily repeats +* :class:`~earthkit.time.sequence.WeeklySequence` for repeats on specific days each week +* :class:`~earthkit.time.sequence.MonthlySequence` for repeats on specific days each month +* :class:`~earthkit.time.sequence.YearlySequence` for repeats on specific days each year + +A sequence object can be created by invoking the corresponding constructor: + +.. code-block:: python + + from earthkit.time import WeeklySequence + from earthkit.time.calendar import MONDAY, WEDNESDAY + seq = WeeklySequence([MONDAY, WEDNESDAY]) + +It can also be loaded from a dictionary using +:meth:`Sequence.from_dict `, or from +a preset file using +:meth:`Sequence.from_resource `. + + +Sequence examples +~~~~~~~~~~~~~~~~~ + +=========================================================== ============================================================================================= +Example Description +=========================================================== ============================================================================================= +``DailySequence()`` Sequence recurring every day +``DailySequence(excludes=[31])`` Sequence recurring every day, except the 31\ :sup:`st` +``WeeklySequence([MONDAY, THURSDAY])`` Sequence recurring every Monday and Thursday +``MonthlySequence([1, 15])`` Sequence recurring every 1\ :sup:`st` and 15\ :sup:`th` of the month +``MonthlySequence([1, 8, 15, 22, 29], excludes=[(2, 29)])`` Sequence recurring every 7 days each month, skipping the 29\ :sup:`th` February +``YearlySequence((12, 25))`` Sequence recurring every year on the 25\ :sup:`th` December +``Sequence.from_resource("ecmwf-4days")`` Pre-defined sequence (equivalent to ``MonthlySequence(range(1, 30, 4), excludes=[(2, 29)])``) +=========================================================== ============================================================================================= + + +Computing individual dates +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the previous or next date in a sequence, use +:meth:`Sequence.previous ` +or :meth:`Sequence.next `: + +.. code-block:: pycon + + >>> seq = WeeklySequence([WEDNESDAY, FRIDAY]) + >>> seq.previous(date(2024, 5, 12)) + datetime.date(2024, 5, 10) + >>> seq.next(date(2024, 5, 10)) + datetime.date(2024, 5, 15) + >>> seq.next(date(2024, 5, 10), strict=False) + datetime.date(2024, 5, 10) + +If the given date is in the sequence, the default behaviour is to skip to the +previous or next one. To keep it, pass ``strict=False``. + +Similarly, the sequence date closest to a given date can be computed using +:meth:`Sequence.nearest `: + +.. code-block:: pycon + + >>> seq = MonthlySequence([1, 15]) + >>> seq.nearest(date(2024, 7, 4)) + datetime.date(2024, 7, 1) + >>> seq.nearest(date(2024, 3, 20)) + datetime.date(2024, 3, 15) + >>> seq.nearest(date(2024, 8, 8)) + datetime.date(2024, 8, 1) + >>> seq.nearest(date(2024, 8, 8), resolve="next") + datetime.date(2024, 8, 15) + +If there is a tie, meaning that the given date is equidistant from the previous +and next date in the sequence, the previous date is returned. This behaviour can +be explicitly controlled by passing ``resolve="previous"`` or +``resolve="next"``. + + +Computing sets of dates +~~~~~~~~~~~~~~~~~~~~~~~ + +To find all the sequence dates falling within a range, use +:meth:`Sequence.range `: + + +.. code-block:: pycon + + >>> print_dates = lambda dates: print(", ".join(d.strftime("%Y%m%d") for d in dates)) + >>> seq = WeeklySequence([0, 2, 4]) + >>> print_dates(seq.range(date(2024, 12, 1), date(2024, 12, 16))) + 20241202, 20241204, 20241206, 20241209, 20241211, 20241213, 20241216 + >>> print_dates(seq.range(date(2024, 12, 1), date(2024, 12, 16), include_end=False)) + 20241202, 20241204, 20241206, 20241209, 20241211, 20241213 + >>> print_dates(seq.range(date(2024, 12, 2), date(2024, 12, 16), include_start=False)) + 20241204, 20241206, 20241209, 20241211, 20241213, 20241216 + +By default, ranges include the given start and end dates. The ``include_start`` +and ``include_end`` arguments control this behaviour. + +To get a given number of dates around one reference, use +:meth:`Sequence.bracket `: + + +.. code-block:: pycon + + >>> seq = WeeklySequence(SATURDAY) + >>> print_dates(seq.bracket(date(1999, 11, 27))) + 19991120, 19991204 + >>> print_dates(seq.bracket(date(1999, 11, 27), strict=False)) + 19991120, 19991127, 19991204 + >>> print_dates(seq.bracket(date(2006, 5, 28), 3)) + 20060513, 20060520, 20060527, 20060603, 20060610, 20060617 + >>> print_dates(seq.bracket(date(2015, 4, 3), (1, 2))) + 20150328, 20150404, 20150411 + >>> print_dates(seq.bracket(date(1993, 7, 17), (2, 1), strict=False)) + 19930703, 19930710, 19930717, 19930724 + +The optional ``num`` argument represents the number of dates to output, +respectively before and after the reference date. If an integer is given, the +same number of dates either side is returned. If ``strict=False`` is passed and +the reference date is in the sequence, it is printed as well (but not counted +towards the numbers requested). + + +Sets of dates for model climates +-------------------------------- + +To get one date per year on the same day as a given reference, use +:meth:`~earthkit.time.climatology.date_range`: + +.. code-block:: pycon + + >>> from earthkit.time import date_range + >>> print_dates(date_range(date(2006, 10, 23), 2000, 2005)) + 20001023, 20011023, 20021023, 20031023, 20041023, 20051023 + >>> print_dates(date_range(date(2005, 6, 2), date(2002, 6, 8), date(2004, 7, 1))) + 20030602, 20040602 + +To combine yearly dates with multiple reference dates taken from a sequence, use +:meth:`~earthkit.time.climatology.model_climate_dates`: + +.. code-block:: pycon + + >>> from earthkit.time import model_climate_dates + >>> seq = Sequence.from_resource("ecmwf-mon-thu") + >>> print_dates(model_climate_dates(date(2023, 8, 6), 2018, 2020, 7, 7, seq)) + 20180731, 20180803, 20180807, 20180810, 20190731, 20190803, 20190807, 20190810, 20200731, 20200803, 20200807, 20200810 diff --git a/docs/guide/cli.rst b/docs/guide/cli.rst new file mode 100644 index 0000000..47e2cd4 --- /dev/null +++ b/docs/guide/cli.rst @@ -0,0 +1,160 @@ +Command-line Interface +====================== + +For a detailed description of the command-line interface, see :ref:`cli_ref`. + + +Date arithmetic +--------------- + +The :program:`earthkit-date` command provides utilities to shift and subtract +dates. For instance, to get the date 13 days before a reference: + +.. code-block:: console + + $ earthkit-date shift 20160219 -13 + 20160206 + +Positive and negative shifts are supported. To do the reverse and compute how +many days there are between two dates: + +.. code-block:: console + + $ earthkit-date diff 20241225 20240314 + 286 + + +Date sequences +-------------- + +The :program:`earthkit-dateseq` command provides various utilities to manipulate +sequences of recurring dates. + + +Specifying a sequence +~~~~~~~~~~~~~~~~~~~~~ + +========================================= =============================================================================== +Example Description +========================================= =============================================================================== +``--daily`` Sequence recurring every day +``--daily --exclude 31`` Sequence recurring every day, except the 31\ :sup:`st` +``--weekly mon/thu`` Sequence recurring every Monday and Thursday +``--monthly 1/15`` Sequence recurring every 1\ :sup:`st` and 15\ :sup:`th` of the month +``--monthly 1/8/15/22/29 --exclude 0229`` Sequence recurring every 7 days each month, skipping the 29\ :sup:`th` February +``--yearly 1225`` Sequence recurring every year on the 25\ :sup:`th` December +``--preset ecmwf-4days`` Pre-defined sequence (equivalent to ``--monthly 1/5/.../29 --exclude 0229``) +========================================= =============================================================================== + +All the arguments can take slash-separated lists to specify multiple occurrences +within a week, month, or year, as well as multiple excludes. Week days can be +numeric (0 is Monday, 1 is Tuesday, etc.) or any unambiguous prefix of the name +(e.g., M, sa, FRIDAY). + + +Computing individual dates +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the previous or next date in a sequence, use ``earthkit-dateseq +previous`` or ``earthkit-dateseq next``: + +.. code-block:: console + + $ earthkit-dateseq previous --weekly wed/fri 20240512 + 20240510 + $ earthkit-dateseq next --weekly wed/fri 20240510 + 20240515 + $ earthkit-dateseq next --weekly wed/fri --inclusive 20240510 + 20240510 + $ earthkit-dateseq next --weekly wed/fri --skip 3 20240510 + 20240524 + +If the given date is in the sequence, the default behaviour is to skip to the +previous or next one. To keep it, add the ``--inclusive`` option. More dates can +be skipped with the ``--skip`` option. + +Similarly, the sequence date closest to a given date can be computed using +``earthkit-dateseq nearest``: + +.. code-block:: console + + $ earthkit-dateseq nearest --monthly 1/15 20240704 + 20240701 + $ earthkit-dateseq nearest --monthly 1/15 20240320 + 20240315 + $ earthkit-dateseq nearest --monthly 1/15 20240808 + 20240801 + $ earthkit-dateseq nearest --monthly 1/15 --resolve next 20240808 + 20240815 + +If there is a tie, meaning that the given date is equidistant from the previous +and next date in the sequence, the previous date is returned. This behaviour can +be explicitly controlled by passing ``--resolve previous`` or ``--resolve +next``. + + +Computing sets of dates +~~~~~~~~~~~~~~~~~~~~~~~ + +To find all the sequence dates falling within a range, use ``earthkit-dateseq range``: + +.. code-block:: console + + $ earthkit-dateseq range --sep ", " --weekly 0/2/4 20241201 20241216 + 20241202, 20241204, 20241206, 20241209, 20241211, 20241213, 20241216 + $ earthkit-dateseq range --sep ", " --weekly 0/2/4 --exclude-end 20241201 20241216 + 20241202, 20241204, 20241206, 20241209, 20241211, 20241213 + $ earthkit-dateseq range --sep ", " --weekly 0/2/4 --exclude-start 20241202 20241216 + 20241204, 20241206, 20241209, 20241211, 20241213, 20241216 + +By default, ranges include the given start and end dates. The +``--exclude-start`` and ``--exclude-end`` flags override this behaviour. + +The output sequences are formatted using the value of ``--sep``, if present, +otherwise each date is printed on a separate line. + +To get a given number of dates around one reference, use ``earthkit-dateseq bracket``: + +.. code-block:: console + + $ earthkit-dateseq bracket --sep ", " --weekly Saturday 19991127 + 19991120, 19991204 + $ earthkit-dateseq bracket --sep ", " --weekly Saturday --inclusive 19991127 + 19991120, 19991127, 19991204 + $ earthkit-dateseq bracket --sep ", " --weekly Saturday 20060528 3 + 20060513, 20060520, 20060527, 20060603, 20060610, 20060617 + $ earthkit-dateseq bracket --sep ", " --weekly Saturday 20150403 1 2 + 20150328, 20150404, 20150411 + $ earthkit-dateseq bracket --sep ", " --weekly Saturday --inclusive 19930717 2 1 + 19930703, 19930710, 19930717, 19930724 + +The last two optional arguments are the number of dates to output, respectively +before and after the reference date. If none is given, one date either side is +returned. If one is given, the same number of dates either side is returned. If +the ``--inclusive`` flag is set and the reference date is in the sequence, it is +printed as well (but not counted towards the numbers requested). + + +Model climate dates +------------------- + +The :program:`earthkit-climdates` command provides utilities to create sets of +dates for model climates. + +To get one date per year on the same day as a given reference, use +``earthkit-climdates range``: + +.. code-block:: console + + $ earthkit-climdates range --sep ", " --from-year 2000 --to-year 2005 20061023 + 20001023, 20011023, 20021023, 20031023, 20041023, 20051023 + $ earthkit-climdates range --sep ", " --from-date 20020608 --to-date 20040701 20050602 + 20030602, 20040602 + +To combine yearly dates with multiple reference dates taken from a sequence, use +``earthkit-climdates mclim``: + +.. code-block:: console + + $ earthkit-climdates mclim --sep ", " --from-year 2018 --to-year 2020 --before 7 --after 7 --preset ecmwf-mon-thu 20230806 + 20180731, 20180803, 20180807, 20180810, 20190731, 20190803, 20190807, 20190810, 20200731, 20200803, 20200807, 20200810 diff --git a/docs/guide/index.rst b/docs/guide/index.rst new file mode 100644 index 0000000..34b485f --- /dev/null +++ b/docs/guide/index.rst @@ -0,0 +1,9 @@ +User's Guide +============ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + api + cli diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..efdd69d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,34 @@ + +Welcome to earthkit-time's documentation! +========================================= + +.. warning:: + This project is in the **BETA** stage of development. Please be aware that + interfaces and functionality may change as the project develops. If this + software is to be used in operational systems you are **strongly advised to + use a released tag in your system configuration**, and you should be willing + to accept incoming changes and bug fixes that require adaptations on your + part. ECMWF **does use** this software in operations and abides by the same + caveats. + + +earthkit-time is a library to manipulate dates and time for weather forecasting. +The library also exposes a command-line interface to its tools. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + guide/index + api/index + cli/index + changelog + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/pyproject.toml b/pyproject.toml index f1f6535..9970c12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "earthkit-time" -version = "0.1.3" +version = "0.1.4" requires-python = ">= 3.8" description = "Date and time manipulation routines for the use of weather data" license = {file = "LICENSE"} readme = "README.md" classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", @@ -25,6 +25,11 @@ classifiers = [ dependencies = ["pyyaml"] [project.optional-dependencies] +docs = [ + "myst-parser", + "Sphinx", + "sphinx-rtd-theme", +] test = [ "pytest >= 8.1", ] @@ -36,6 +41,7 @@ earthkit-date = "earthkit.time.cli.date:main" [project.urls] Homepage = "https://github.com/ecmwf/earthkit-time/" +Documentation = "https://earthkit-time.readthedocs.io" [tool.isort] profile = "black" diff --git a/src/earthkit/time/calendar.py b/src/earthkit/time/calendar.py index 7cb70eb..f504338 100644 --- a/src/earthkit/time/calendar.py +++ b/src/earthkit/time/calendar.py @@ -5,6 +5,8 @@ class Weekday(IntEnum): + """:class:`enum.IntEnum` representing week days""" + MONDAY = 0 TUESDAY = 1 WEDNESDAY = 2 @@ -82,6 +84,8 @@ def day_exists(year: int, month: int, day: int) -> bool: class MonthInYear: + """Represent a given month in a year""" + year: int month: int @@ -103,14 +107,17 @@ def __contains__(self, day: Union[int, date]) -> bool: return True def length(self) -> int: + """Returns the number of days in the given month""" return month_length(self.year, self.month) def next(self) -> "MonthInYear": + """Return the following month""" d, m = divmod(self.month, 12) m += 1 return MonthInYear(self.year + d, m) def previous(self) -> "MonthInYear": + """Return the previous month""" d, m = divmod(self.month - 2, 12) m += 1 return MonthInYear(self.year + d, m) @@ -139,6 +146,7 @@ def parse_mmdd(arg: Union[Tuple[int, int], str]) -> Tuple[int, int]: def parse_date(arg: Union[str, Tuple[int, int, int]]) -> date: + """Convert triples of ints or YYYYMMDD strings into date objects""" if not isinstance(arg, str): y, m, d = arg if not day_exists(y, m, d): diff --git a/src/earthkit/time/cli/climatology.py b/src/earthkit/time/cli/climatology.py index 6ef02af..bd11980 100644 --- a/src/earthkit/time/cli/climatology.py +++ b/src/earthkit/time/cli/climatology.py @@ -1,4 +1,5 @@ import argparse +import textwrap from typing import List, Optional from ..calendar import parse_date @@ -65,11 +66,14 @@ def get_parser() -> argparse.ArgumentParser: "mclim", model_climate_action, help="compute sets of dates for model climatologies", - description="""Compute sets of dates for model climatologies - - This combines a climatological range (same day in multiple years) and a - recurring source (e.g. twice a week). - """, + description=textwrap.dedent( + """\ + Compute sets of dates for model climatologies + + This combines a climatological range (same day in multiple years) and a + recurring source (e.g. twice a week). + """ + ), epilog=SEQ_EPILOG + "\n" + SEP_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter, ) diff --git a/src/earthkit/time/cli/sequence.py b/src/earthkit/time/cli/sequence.py index 0052725..950eadc 100644 --- a/src/earthkit/time/cli/sequence.py +++ b/src/earthkit/time/cli/sequence.py @@ -15,12 +15,18 @@ def seq_next_action(parser: argparse.ArgumentParser, args: argparse.Namespace): seq = create_sequence(parser, args) - print(format_date(seq.next(args.date, strict=(not args.inclusive)))) + new = seq.next(args.date, strict=(not args.inclusive)) + for _ in range(args.skip): + new = seq.next(new, strict=True) + print(format_date(new)) def seq_prev_action(parser: argparse.ArgumentParser, args: argparse.Namespace): seq = create_sequence(parser, args) - print(format_date(seq.previous(args.date, strict=(not args.inclusive)))) + new = seq.previous(args.date, strict=(not args.inclusive)) + for _ in range(args.skip): + new = seq.previous(new, strict=True) + print(format_date(new)) def seq_nearest_action(parser: argparse.ArgumentParser, args: argparse.Namespace): @@ -75,6 +81,12 @@ def get_parser() -> argparse.ArgumentParser: action="store_true", help="if the given date is in the sequence, return it", ) + next_action.add_argument( + "--skip", + type=int, + default=0, + help="if set, skip over that number of dates", + ) prev_action = parser.add_action( "previous", @@ -91,6 +103,12 @@ def get_parser() -> argparse.ArgumentParser: action="store_true", help="if the given date is in the sequence, return it", ) + prev_action.add_argument( + "--skip", + type=int, + default=0, + help="if set, skip over that number of dates", + ) nearest_action = parser.add_action( "nearest", diff --git a/src/earthkit/time/climatology.py b/src/earthkit/time/climatology.py index 328485c..580d84f 100644 --- a/src/earthkit/time/climatology.py +++ b/src/earthkit/time/climatology.py @@ -21,13 +21,13 @@ def date_range( Parameters ---------- - reference: date + reference: :class:`datetime.date` Reference date setting the fixed part in the sequence (e.g., month and day for a yearly recurrence) - start: date or int + start: :class:`datetime.date` or int Start of the period. Either a full date or a meaningful identifier (e.g. year for a yearly recurrence) - end: date or int + end: :class:`datetime.date` or int End of the period. Included in the sequence unless ``include_endpoint`` is ``False`` recurrence: "yearly" @@ -37,7 +37,7 @@ def date_range( Returns ------- - date iterator + :class:`datetime.date` iterator Sequence of dates Examples @@ -86,23 +86,23 @@ def model_climate_dates( Parameters ---------- - reference: date + reference: :class:`datetime.date` Reference date for the climate - start: date or int + start: :class:`datetime.date` or int Start of the climatological period. Either a full date or a year - end: date or int + end: :class:`datetime.date` or int End of the climatological period. Either a full date or a year - before: timedelta or int + before: :class:`datetime.timedelta` or int Cut-off before the reference date. Either a timedelta or a number of days - after: timedelta or int + after: :class:`datetime.timedelta` or int Cut-off after the reference date. Either a timedelta or a number of days - sequence: `Sequence` + sequence: :class:`earthkit.time.sequence.Sequence` Sequence of available dates in the reference set Returns ------- - date iterator + :class:`datetime.date` iterator Sequence of dates Examples diff --git a/src/earthkit/time/sequence.py b/src/earthkit/time/sequence.py index 4bfb70b..afb3d94 100644 --- a/src/earthkit/time/sequence.py +++ b/src/earthkit/time/sequence.py @@ -98,7 +98,7 @@ def bracket( Parameters ---------- - reference: date + reference: :class:`datetime.date` Reference date num: int or (int, int) tuple Number of dates to include either side of ``reference``. If a single @@ -134,8 +134,8 @@ def _from_dict(cls, seq_dict: dict) -> "Sequence": """Create a specific sequence from the given dictionary Dictionary contents can vary depending on the sequence. Frequent items are: - * days: list of recurring days - * excludes: specification of which days to skip + * ``days``: list of recurring days + * ``excludes``: specification of which days to skip """ raise NotImplementedError @@ -161,10 +161,11 @@ def from_dict(cls, seq_dict: dict) -> "Sequence": ``yearly``. Dictionary contents can vary depending on the sequence. Frequent items are: - * days: list of recurring days - * excludes: specification of which days to skip - Raises `ValueError` if the type is unknown + * ``days``: list of recurring days + * ``excludes``: specification of which days to skip + + Raises :class:`ValueError` if the type is unknown """ if "type" not in seq_dict: raise ValueError("Sequence dictionary must contain `type` key") @@ -181,7 +182,7 @@ def from_resource(cls, name: str) -> "Sequence": ``earthkit.time.data.sequences`` or ``EARTHKIT_TIME_SEQ_PATH``, without the extension), or the path to a YAML file - Raises `FileNotFoundError` if no corresponding resource is found + Raises :class:`FileNotFoundError` if no corresponding resource is found """ path = name if os.path.isfile(name) else None seq_dict = load_yaml( @@ -197,7 +198,8 @@ class DailySequence(Sequence, seqname="daily"): Any day number (in the month) present in ``excludes`` will be skipped - Can be created from a `dict` with items: + Can be created from a :class:`dict` with items: + * ``type``: ``"daily"`` * ``excludes``: (list of int, optional) days of the month to exclude """ @@ -219,7 +221,8 @@ def _from_dict(cls, seq_dict: dict) -> Sequence: class WeeklySequence(Sequence, seqname="weekly"): """Sequence of dates happening on given days of each week - Can be created from a `dict` with items: + Can be created from a :class:`dict` with items: + * ``type``: ``"weekly"`` * ``days``: (int, str, list of int, list of str) days of the week, either numeric (0 = Monday, ..., 6 = Sunday) or unambiguous prefixes of names @@ -283,7 +286,8 @@ class MonthlySequence(Sequence, seqname="monthly"): Any ``(month, day)`` tuple present in ``excludes`` will be skipped - Can be created from a `dict` with items: + Can be created from a :class:`dict` with items: + * ``type``: ``"monthly"`` * ``days``: (int, list of int) days of the month (1-31) * ``excludes``: (list of pairs of int, list of str, optional) days of the @@ -369,7 +373,8 @@ def _from_dict(cls, seq_dict: dict) -> Sequence: class YearlySequence(Sequence, seqname="yearly"): """Sequence of dates happening on given days of each year (in (month, day) format) - Can be created from a `dict` with items: + Can be created from a :class:`dict` with items: + * ``type``: ``"yearly"`` * ``days``: (str, pair of int, list of str, list of pairs of int) days of the year either in "MMDD" or in (month, day) form (1-12, 1-31) diff --git a/tests/cli/test_sequence_cli.py b/tests/cli/test_sequence_cli.py index 6afac88..243d07a 100644 --- a/tests/cli/test_sequence_cli.py +++ b/tests/cli/test_sequence_cli.py @@ -48,6 +48,26 @@ "20030228", id="yearly-excludes-inc", ), + pytest.param( + {"monthly": [5, 20], "skip": 2, "date": date(2007, 4, 3)}, + "20070505", + id="skip", + ), + pytest.param( + {"monthly": [5, 20], "skip": 2, "date": date(2007, 4, 5)}, + "20070520", + id="skip-exc", + ), + pytest.param( + { + "monthly": [5, 20], + "skip": 2, + "inclusive": True, + "date": date(2007, 4, 5), + }, + "20070505", + id="skip-inc", + ), ], ) def test_seq_next(args: dict, expected: str, capsys: pytest.CaptureFixture[str]): @@ -58,6 +78,7 @@ def test_seq_next(args: dict, expected: str, capsys: pytest.CaptureFixture[str]) args.setdefault("yearly", None) args.setdefault("exclude", []) args.setdefault("inclusive", False) + args.setdefault("skip", 0) args = argparse.Namespace(**args) seq_next_action(parser, args) captured = capsys.readouterr() @@ -89,6 +110,26 @@ def test_seq_next(args: dict, expected: str, capsys: pytest.CaptureFixture[str]) "20061015", id="monthly", ), + pytest.param( + {"monthly": [5, 20], "skip": 2, "date": date(2007, 4, 10)}, + "20070305", + id="skip", + ), + pytest.param( + {"monthly": [5, 20], "skip": 2, "date": date(2007, 4, 5)}, + "20070220", + id="skip-exc", + ), + pytest.param( + { + "monthly": [5, 20], + "skip": 2, + "inclusive": True, + "date": date(2007, 4, 5), + }, + "20070305", + id="skip-inc", + ), ], ) def test_seq_prev(args: dict, expected: str, capsys: pytest.CaptureFixture[str]): @@ -99,6 +140,7 @@ def test_seq_prev(args: dict, expected: str, capsys: pytest.CaptureFixture[str]) args.setdefault("yearly", None) args.setdefault("exclude", []) args.setdefault("inclusive", False) + args.setdefault("skip", 0) args = argparse.Namespace(**args) seq_prev_action(parser, args) captured = capsys.readouterr()