anemoi-utils 0.3.13__tar.gz → 0.3.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of anemoi-utils might be problematic. Click here for more details.
- anemoi_utils-0.3.15/.github/workflows/changelog-pr-update.yml +15 -0
- anemoi_utils-0.3.15/.github/workflows/ci.yml +40 -0
- anemoi_utils-0.3.15/.github/workflows/label-public-pr.yml +10 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/.github/workflows/python-publish.yml +3 -23
- anemoi_utils-0.3.15/.github/workflows/readthedocs-pr-update.yml +22 -0
- anemoi_utils-0.3.15/CHANGELOG.md +39 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/PKG-INFO +2 -1
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/pyproject.toml +2 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/_version.py +2 -2
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/cli.py +7 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/dates.py +144 -18
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/humanize.py +59 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/s3.py +1 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/text.py +85 -11
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi_utils.egg-info/PKG-INFO +2 -1
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi_utils.egg-info/SOURCES.txt +6 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi_utils.egg-info/requires.txt +1 -0
- anemoi_utils-0.3.15/tests/test_frequency.py +37 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/.gitignore +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/.pre-commit-config.yaml +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/.readthedocs.yaml +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/LICENSE +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/README.md +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/Makefile +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/_static/style.css +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/conf.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/index.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/installing.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/setup.cfg +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/caching.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/checkpoints.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/config.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/provenance.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi/utils/timer.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/tests/test_dates.py +0 -0
- {anemoi_utils-0.3.13 → anemoi_utils-0.3.15}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Check Changelog Update on PR
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- develop
|
|
8
|
+
jobs:
|
|
9
|
+
Check-Changelog:
|
|
10
|
+
name: Check Changelog Action
|
|
11
|
+
runs-on: ubuntu-20.04
|
|
12
|
+
steps:
|
|
13
|
+
- uses: tarides/changelog-check-action@v2
|
|
14
|
+
with:
|
|
15
|
+
changelog: CHANGELOG.md
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
# Trigger the workflow on push to master or develop, except tag creation
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- 'main'
|
|
8
|
+
- 'develop'
|
|
9
|
+
tags-ignore:
|
|
10
|
+
- '**'
|
|
11
|
+
|
|
12
|
+
# Trigger the workflow on pull request
|
|
13
|
+
pull_request: ~
|
|
14
|
+
|
|
15
|
+
# Trigger the workflow manually installs
|
|
16
|
+
workflow_dispatch: ~
|
|
17
|
+
|
|
18
|
+
# Trigger after public PR approved for CI
|
|
19
|
+
pull_request_target:
|
|
20
|
+
types: [labeled]
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
# Run CI including downstream packages on self-hosted runners
|
|
24
|
+
downstream-ci:
|
|
25
|
+
name: downstream-ci
|
|
26
|
+
if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
|
|
27
|
+
uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci.yml@main
|
|
28
|
+
with:
|
|
29
|
+
anemoi-utils: ecmwf/anemoi-utils@${{ github.event.pull_request.head.sha || github.sha }}
|
|
30
|
+
codecov_upload: true
|
|
31
|
+
secrets: inherit
|
|
32
|
+
|
|
33
|
+
# Build downstream packages on HPC
|
|
34
|
+
downstream-ci-hpc:
|
|
35
|
+
name: downstream-ci-hpc
|
|
36
|
+
if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
|
|
37
|
+
uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci.yml@main
|
|
38
|
+
with:
|
|
39
|
+
anemoi-utils: ecmwf/anemoi-utils@${{ github.event.pull_request.head.sha || github.sha }}
|
|
40
|
+
secrets: inherit
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
steps:
|
|
36
36
|
- uses: actions/checkout@v4
|
|
37
37
|
|
|
38
|
-
- uses: actions/setup-python@
|
|
38
|
+
- uses: actions/setup-python@v5
|
|
39
39
|
with:
|
|
40
40
|
python-version: ${{ matrix.python-version }}
|
|
41
41
|
|
|
@@ -49,26 +49,6 @@ jobs:
|
|
|
49
49
|
|
|
50
50
|
deploy:
|
|
51
51
|
|
|
52
|
-
if: ${{ github.event_name == 'release' }}
|
|
53
|
-
runs-on: ubuntu-latest
|
|
54
52
|
needs: [checks, quality]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- uses: actions/checkout@v4
|
|
58
|
-
|
|
59
|
-
- name: Set up Python
|
|
60
|
-
uses: actions/setup-python@v2
|
|
61
|
-
with:
|
|
62
|
-
python-version: 3.x
|
|
63
|
-
|
|
64
|
-
- name: Install dependencies
|
|
65
|
-
run: |
|
|
66
|
-
python -m pip install --upgrade pip
|
|
67
|
-
pip install build wheel twine
|
|
68
|
-
- name: Build and publish
|
|
69
|
-
env:
|
|
70
|
-
TWINE_USERNAME: __token__
|
|
71
|
-
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
72
|
-
run: |
|
|
73
|
-
python -m build
|
|
74
|
-
twine upload dist/*
|
|
53
|
+
uses: ecmwf-actions/reusable-workflows/.github/workflows/cd-pypi.yml@v2
|
|
54
|
+
secrets: inherit
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Read the Docs PR Preview
|
|
2
|
+
on:
|
|
3
|
+
pull_request_target:
|
|
4
|
+
types:
|
|
5
|
+
- opened
|
|
6
|
+
- synchronize
|
|
7
|
+
- reopened
|
|
8
|
+
# Execute this action only on PRs that touch
|
|
9
|
+
# documentation files.
|
|
10
|
+
paths:
|
|
11
|
+
- "docs/**"
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
pull-requests: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
documentation-links:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: readthedocs/actions/preview@v1
|
|
21
|
+
with:
|
|
22
|
+
project-slug: "anemoi-utils"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Please add your functional changes to the appropriate section in the PR.
|
|
9
|
+
Keep it human-readable, your future self will thank you!
|
|
10
|
+
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
## [0.3.0] - Initial Release, utility functions
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Command line interface utility
|
|
23
|
+
|
|
24
|
+
## [0.2.0] - Initial Release, utility functions
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- updated documentation
|
|
28
|
+
|
|
29
|
+
## [0.1.0] - Initial Release, utility functions
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- Documentation
|
|
33
|
+
- Initial implementation for a series of utility functions for used by the rest of the Anemoi packages
|
|
34
|
+
|
|
35
|
+
<!-- Add Git Diffs for Links above -->
|
|
36
|
+
[unreleased]: https://github.com/ecmwf/anemoi-utils/compare/0.3.13...HEAD
|
|
37
|
+
https://github.com/ecmwf/anemoi-utils/compare/0.2.0...0.3.0
|
|
38
|
+
https://github.com/ecmwf/anemoi-utils/compare/0.1.0...0.2.0
|
|
39
|
+
[0.1.0]: https://github.com/ecmwf/anemoi-utils/releases/tag/0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.15
|
|
4
4
|
Summary: A package to hold various functions to support training of ML models on ECMWF data.
|
|
5
5
|
Author-email: "European Centre for Medium-Range Weather Forecasts (ECMWF)" <software.support@ecmwf.int>
|
|
6
6
|
License: Apache License
|
|
@@ -223,6 +223,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
223
223
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
224
224
|
Requires-Python: >=3.9
|
|
225
225
|
License-File: LICENSE
|
|
226
|
+
Requires-Dist: isodate
|
|
226
227
|
Requires-Dist: pyyaml
|
|
227
228
|
Requires-Dist: tomli
|
|
228
229
|
Requires-Dist: tqdm
|
|
@@ -12,6 +12,11 @@ import os
|
|
|
12
12
|
import sys
|
|
13
13
|
import traceback
|
|
14
14
|
|
|
15
|
+
try:
|
|
16
|
+
import argcomplete
|
|
17
|
+
except ImportError:
|
|
18
|
+
argcomplete = None
|
|
19
|
+
|
|
15
20
|
LOG = logging.getLogger(__name__)
|
|
16
21
|
|
|
17
22
|
|
|
@@ -100,6 +105,8 @@ def register_commands(here, package, select, fail=None):
|
|
|
100
105
|
def cli_main(version, description, commands):
|
|
101
106
|
parser = make_parser(description, commands)
|
|
102
107
|
args, unknown = parser.parse_known_args()
|
|
108
|
+
if argcomplete:
|
|
109
|
+
argcomplete.autocomplete(parser)
|
|
103
110
|
|
|
104
111
|
if args.version:
|
|
105
112
|
print(version)
|
|
@@ -8,21 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
import calendar
|
|
10
10
|
import datetime
|
|
11
|
+
import re
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def normalise_frequency(frequency):
|
|
16
|
-
if isinstance(frequency, int):
|
|
17
|
-
return frequency
|
|
18
|
-
assert isinstance(frequency, str), (type(frequency), frequency)
|
|
13
|
+
import isodate
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
v = int(frequency[:-1])
|
|
22
|
-
return {"h": v, "d": v * 24}[unit]
|
|
15
|
+
from .hindcasts import HindcastDatesTimes
|
|
23
16
|
|
|
24
17
|
|
|
25
|
-
def
|
|
18
|
+
def _no_time_zone(date):
|
|
26
19
|
"""Remove time zone information from a date.
|
|
27
20
|
|
|
28
21
|
Parameters
|
|
@@ -40,13 +33,15 @@ def no_time_zone(date):
|
|
|
40
33
|
|
|
41
34
|
|
|
42
35
|
# this function is use in anemoi-datasets
|
|
43
|
-
def as_datetime(date):
|
|
36
|
+
def as_datetime(date, keep_time_zone=False):
|
|
44
37
|
"""Convert a date to a datetime object, removing any time zone information.
|
|
45
38
|
|
|
46
39
|
Parameters
|
|
47
40
|
----------
|
|
48
41
|
date : datetime.date or datetime.datetime or str
|
|
49
42
|
The date to convert.
|
|
43
|
+
keep_time_zone : bool, optional
|
|
44
|
+
If True, the time zone information is kept, by default False.
|
|
50
45
|
|
|
51
46
|
Returns
|
|
52
47
|
-------
|
|
@@ -54,18 +49,150 @@ def as_datetime(date):
|
|
|
54
49
|
The datetime object.
|
|
55
50
|
"""
|
|
56
51
|
|
|
52
|
+
tidy = _no_time_zone if not keep_time_zone else lambda x: x
|
|
53
|
+
|
|
57
54
|
if isinstance(date, datetime.datetime):
|
|
58
|
-
return
|
|
55
|
+
return tidy(date)
|
|
59
56
|
|
|
60
57
|
if isinstance(date, datetime.date):
|
|
61
|
-
return
|
|
58
|
+
return tidy(datetime.datetime(date.year, date.month, date.day))
|
|
62
59
|
|
|
63
60
|
if isinstance(date, str):
|
|
64
|
-
return
|
|
61
|
+
return tidy(datetime.datetime.fromisoformat(date))
|
|
65
62
|
|
|
66
63
|
raise ValueError(f"Invalid date type: {type(date)}")
|
|
67
64
|
|
|
68
65
|
|
|
66
|
+
def frequency_to_timedelta(frequency):
|
|
67
|
+
"""Convert a frequency to a timedelta object.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
frequency : int or str or datetime.timedelta
|
|
72
|
+
The frequency to convert. If an integer, it is assumed to be in hours. If a string, it can be in the format:
|
|
73
|
+
|
|
74
|
+
- "1h" for 1 hour
|
|
75
|
+
- "1d" for 1 day
|
|
76
|
+
- "1m" for 1 minute
|
|
77
|
+
- "1s" for 1 second
|
|
78
|
+
- "1:30" for 1 hour and 30 minutes
|
|
79
|
+
- "1:30:10" for 1 hour, 30 minutes and 10 seconds
|
|
80
|
+
- "PT10M" for 10 minutes (ISO8601)
|
|
81
|
+
|
|
82
|
+
If a timedelta object is provided, it is returned as is.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
datetime.timedelta
|
|
87
|
+
The timedelta object.
|
|
88
|
+
|
|
89
|
+
Raises
|
|
90
|
+
------
|
|
91
|
+
ValueError
|
|
92
|
+
Exception raised if the frequency cannot be converted to a timedelta.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
if isinstance(frequency, datetime.timedelta):
|
|
96
|
+
return frequency
|
|
97
|
+
|
|
98
|
+
if isinstance(frequency, int):
|
|
99
|
+
return datetime.timedelta(hours=frequency)
|
|
100
|
+
|
|
101
|
+
assert isinstance(frequency, str), (type(frequency), frequency)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
return frequency_to_timedelta(int(frequency))
|
|
105
|
+
except ValueError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
if re.match(r"^\d+[hdms]$", frequency, re.IGNORECASE):
|
|
109
|
+
unit = frequency[-1].lower()
|
|
110
|
+
v = int(frequency[:-1])
|
|
111
|
+
unit = {"h": "hours", "d": "days", "s": "seconds", "m": "minutes"}[unit]
|
|
112
|
+
return datetime.timedelta(**{unit: v})
|
|
113
|
+
|
|
114
|
+
m = frequency.split(":")
|
|
115
|
+
if len(m) == 2:
|
|
116
|
+
return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]))
|
|
117
|
+
|
|
118
|
+
if len(m) == 3:
|
|
119
|
+
return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]), seconds=int(m[2]))
|
|
120
|
+
|
|
121
|
+
# ISO8601
|
|
122
|
+
try:
|
|
123
|
+
return isodate.parse_duration(frequency)
|
|
124
|
+
except isodate.isoerror.ISO8601Error:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
raise ValueError(f"Cannot convert frequency {frequency} to timedelta")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def frequency_to_string(frequency):
|
|
131
|
+
"""Convert a frequency (i.e. a datetime.timedelta) to a string.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
frequency : datetime.timedelta
|
|
136
|
+
The frequency to convert.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
str
|
|
141
|
+
A string representation of the frequency.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
frequency = frequency_to_timedelta(frequency)
|
|
145
|
+
|
|
146
|
+
total_seconds = frequency.total_seconds()
|
|
147
|
+
assert int(total_seconds) == total_seconds, total_seconds
|
|
148
|
+
total_seconds = int(total_seconds)
|
|
149
|
+
|
|
150
|
+
seconds = total_seconds
|
|
151
|
+
|
|
152
|
+
days = seconds // (24 * 3600)
|
|
153
|
+
seconds %= 24 * 3600
|
|
154
|
+
hours = seconds // 3600
|
|
155
|
+
seconds %= 3600
|
|
156
|
+
minutes = seconds // 60
|
|
157
|
+
seconds %= 60
|
|
158
|
+
|
|
159
|
+
if days > 0 and hours == 0 and minutes == 0 and seconds == 0:
|
|
160
|
+
return f"{days}d"
|
|
161
|
+
|
|
162
|
+
if days == 0 and hours > 0 and minutes == 0 and seconds == 0:
|
|
163
|
+
return f"{hours}h"
|
|
164
|
+
|
|
165
|
+
if days == 0 and hours == 0 and minutes > 0 and seconds == 0:
|
|
166
|
+
return f"{minutes}m"
|
|
167
|
+
|
|
168
|
+
if days == 0 and hours == 0 and minutes == 0 and seconds > 0:
|
|
169
|
+
return f"{seconds}s"
|
|
170
|
+
|
|
171
|
+
if days > 0:
|
|
172
|
+
return f"{total_seconds}s"
|
|
173
|
+
|
|
174
|
+
return str(frequency)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def frequency_to_seconds(frequency):
|
|
178
|
+
"""Convert a frequency to seconds.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
frequency : _type_
|
|
183
|
+
_description_
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
int
|
|
188
|
+
Number of seconds.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
result = frequency_to_timedelta(frequency).total_seconds()
|
|
192
|
+
assert int(result) == result, result
|
|
193
|
+
return int(result)
|
|
194
|
+
|
|
195
|
+
|
|
69
196
|
DOW = {
|
|
70
197
|
"monday": 0,
|
|
71
198
|
"tuesday": 1,
|
|
@@ -142,7 +269,7 @@ class DateTimes:
|
|
|
142
269
|
"""
|
|
143
270
|
self.start = as_datetime(start)
|
|
144
271
|
self.end = as_datetime(end)
|
|
145
|
-
self.increment =
|
|
272
|
+
self.increment = frequency_to_timedelta(increment)
|
|
146
273
|
self.day_of_month = _make_day(day_of_month)
|
|
147
274
|
self.day_of_week = _make_week(day_of_week)
|
|
148
275
|
self.calendar_months = _make_months(calendar_months)
|
|
@@ -273,8 +400,7 @@ def datetimes_factory(*args, **kwargs):
|
|
|
273
400
|
|
|
274
401
|
kwargs = kwargs.copy()
|
|
275
402
|
if "frequency" in kwargs:
|
|
276
|
-
|
|
277
|
-
kwargs["increment"] = normalise_frequency(freq)
|
|
403
|
+
kwargs["increment"] = kwargs.pop("frequency")
|
|
278
404
|
return DateTimes(**kwargs)
|
|
279
405
|
|
|
280
406
|
if not any((isinstance(x, dict) or isinstance(x, list)) for x in args):
|
|
@@ -15,6 +15,8 @@ import re
|
|
|
15
15
|
import warnings
|
|
16
16
|
from collections import defaultdict
|
|
17
17
|
|
|
18
|
+
from anemoi.utils.dates import as_datetime
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def bytes_to_human(n: float) -> str:
|
|
20
22
|
"""Convert a number of bytes to a human readable string
|
|
@@ -625,3 +627,60 @@ def shorten_list(lst, max_length=5):
|
|
|
625
627
|
if isinstance(lst, tuple):
|
|
626
628
|
return tuple(result)
|
|
627
629
|
return result
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _compress_dates(dates):
|
|
633
|
+
dates = sorted(dates)
|
|
634
|
+
if len(dates) < 3:
|
|
635
|
+
yield dates
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
prev = first = dates.pop(0)
|
|
639
|
+
curr = dates.pop(0)
|
|
640
|
+
delta = curr - prev
|
|
641
|
+
while curr - prev == delta:
|
|
642
|
+
prev = curr
|
|
643
|
+
if not dates:
|
|
644
|
+
break
|
|
645
|
+
curr = dates.pop(0)
|
|
646
|
+
|
|
647
|
+
yield (first, prev, delta)
|
|
648
|
+
if dates:
|
|
649
|
+
yield from _compress_dates([curr] + dates)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def compress_dates(dates):
|
|
653
|
+
"""Compress a list of dates into a human-readable format.
|
|
654
|
+
|
|
655
|
+
Parameters
|
|
656
|
+
----------
|
|
657
|
+
dates : list
|
|
658
|
+
A list of dates, as datetime objects or strings.
|
|
659
|
+
|
|
660
|
+
Returns
|
|
661
|
+
-------
|
|
662
|
+
str
|
|
663
|
+
A human-readable string representing the compressed dates.
|
|
664
|
+
"""
|
|
665
|
+
|
|
666
|
+
dates = [as_datetime(_) for _ in dates]
|
|
667
|
+
result = []
|
|
668
|
+
|
|
669
|
+
for n in _compress_dates(dates):
|
|
670
|
+
if isinstance(n, list):
|
|
671
|
+
result.extend([str(_) for _ in n])
|
|
672
|
+
else:
|
|
673
|
+
result.append(" ".join([str(n[0]), "to", str(n[1]), "by", str(n[2])]))
|
|
674
|
+
|
|
675
|
+
return result
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def print_dates(dates):
|
|
679
|
+
"""Print a list of dates in a human-readable format.
|
|
680
|
+
|
|
681
|
+
Parameters
|
|
682
|
+
----------
|
|
683
|
+
dates : list
|
|
684
|
+
A list of dates, as datetime objects or strings.
|
|
685
|
+
"""
|
|
686
|
+
print(compress_dates(dates))
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
"""Text utilities"""
|
|
9
9
|
|
|
10
|
+
import re
|
|
10
11
|
from collections import defaultdict
|
|
11
12
|
|
|
12
13
|
# https://en.wikipedia.org/wiki/Box-drawing_character
|
|
@@ -32,6 +33,62 @@ def dotted_line(width=84) -> str:
|
|
|
32
33
|
return "┈" * width
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
# Regular expression to match ANSI escape codes
|
|
37
|
+
_ansi_escape = re.compile(r"\x1b\[([0-9;]*[mGKH])")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _has_ansi_escape(s):
|
|
41
|
+
return _ansi_escape.search(s) is not None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _split_tokens(s):
|
|
45
|
+
"""Split a string into a list of visual characters with their lenghts."""
|
|
46
|
+
from wcwidth import wcswidth
|
|
47
|
+
|
|
48
|
+
initial = s
|
|
49
|
+
out = []
|
|
50
|
+
|
|
51
|
+
# Function to probe the number of bytes needed to encode the first character
|
|
52
|
+
def probe_utf8(s):
|
|
53
|
+
for i in range(1, 5):
|
|
54
|
+
try:
|
|
55
|
+
s[:i].encode("utf-8")
|
|
56
|
+
except UnicodeEncodeError:
|
|
57
|
+
return i - 1
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
while s:
|
|
61
|
+
match = _ansi_escape.match(s)
|
|
62
|
+
if match:
|
|
63
|
+
token = match.group(0)
|
|
64
|
+
s = s[len(token) :]
|
|
65
|
+
out.append((token, 0))
|
|
66
|
+
else:
|
|
67
|
+
i = probe_utf8(s)
|
|
68
|
+
token = s[:i]
|
|
69
|
+
s = s[i:]
|
|
70
|
+
out.append((token, wcswidth(token)))
|
|
71
|
+
|
|
72
|
+
assert "".join(token for (token, _) in out) == initial, (out, initial)
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def visual_len(s):
|
|
77
|
+
"""Compute the length of a string as it appears on the terminal."""
|
|
78
|
+
if isinstance(s, str):
|
|
79
|
+
s = _split_tokens(s)
|
|
80
|
+
assert isinstance(s, (tuple, list)), (type(s), s)
|
|
81
|
+
if len(s) == 0:
|
|
82
|
+
return 0
|
|
83
|
+
for _ in s:
|
|
84
|
+
assert isinstance(_, tuple), s
|
|
85
|
+
assert len(_) == 2, s
|
|
86
|
+
n = 0
|
|
87
|
+
for _, width in s:
|
|
88
|
+
n += width
|
|
89
|
+
return n
|
|
90
|
+
|
|
91
|
+
|
|
35
92
|
def boxed(text, min_width=80, max_width=80) -> str:
|
|
36
93
|
"""Put a box around a text
|
|
37
94
|
|
|
@@ -57,27 +114,44 @@ def boxed(text, min_width=80, max_width=80) -> str:
|
|
|
57
114
|
|
|
58
115
|
"""
|
|
59
116
|
|
|
60
|
-
lines =
|
|
61
|
-
|
|
117
|
+
lines = []
|
|
118
|
+
for line in text.split("\n"):
|
|
119
|
+
line = line.strip()
|
|
120
|
+
line = _split_tokens(line)
|
|
121
|
+
lines.append(line)
|
|
122
|
+
|
|
123
|
+
width = max(visual_len(_) for _ in lines)
|
|
62
124
|
|
|
63
125
|
if min_width is not None:
|
|
64
126
|
width = max(width, min_width)
|
|
65
127
|
|
|
66
128
|
if max_width is not None:
|
|
129
|
+
|
|
130
|
+
def shorten_line(line, max_width):
|
|
131
|
+
if visual_len(line) > max_width:
|
|
132
|
+
while visual_len(line) >= max_width:
|
|
133
|
+
line = line[:-1]
|
|
134
|
+
line.append(("…", 1))
|
|
135
|
+
return line
|
|
136
|
+
|
|
67
137
|
width = min(width, max_width)
|
|
68
|
-
lines = []
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
138
|
+
lines = [shorten_line(line, max_width) for line in lines]
|
|
139
|
+
|
|
140
|
+
def pad_line(line, width):
|
|
141
|
+
line = line + [" "] * (width - visual_len(line))
|
|
142
|
+
return line
|
|
143
|
+
|
|
144
|
+
lines = [pad_line(line, width) for line in lines]
|
|
74
145
|
|
|
75
146
|
box = []
|
|
76
147
|
box.append("┌" + "─" * (width + 2) + "┐")
|
|
77
148
|
for line in lines:
|
|
78
|
-
|
|
79
|
-
|
|
149
|
+
s = "".join(_[0] for _ in line)
|
|
150
|
+
if _has_ansi_escape(s):
|
|
151
|
+
s += "\x1b[0m"
|
|
152
|
+
box.append(f"│ {s} │")
|
|
80
153
|
box.append("└" + "─" * (width + 2) + "┘")
|
|
154
|
+
|
|
81
155
|
return "\n".join(box)
|
|
82
156
|
|
|
83
157
|
|
|
@@ -241,7 +315,7 @@ def table(rows, header, align, margin=0):
|
|
|
241
315
|
['B', 120, 1],
|
|
242
316
|
['C', 9, 123]],
|
|
243
317
|
['C1', 'C2', 'C3'],
|
|
244
|
-
['<', '>', '>'])
|
|
318
|
+
['<', '>', '>'])
|
|
245
319
|
C1 │ C2 │ C3
|
|
246
320
|
───┼─────┼────
|
|
247
321
|
Aa │ 12 │ 5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.15
|
|
4
4
|
Summary: A package to hold various functions to support training of ML models on ECMWF data.
|
|
5
5
|
Author-email: "European Centre for Medium-Range Weather Forecasts (ECMWF)" <software.support@ecmwf.int>
|
|
6
6
|
License: Apache License
|
|
@@ -223,6 +223,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
223
223
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
224
224
|
Requires-Python: >=3.9
|
|
225
225
|
License-File: LICENSE
|
|
226
|
+
Requires-Dist: isodate
|
|
226
227
|
Requires-Dist: pyyaml
|
|
227
228
|
Requires-Dist: tomli
|
|
228
229
|
Requires-Dist: tqdm
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
.gitignore
|
|
2
2
|
.pre-commit-config.yaml
|
|
3
3
|
.readthedocs.yaml
|
|
4
|
+
CHANGELOG.md
|
|
4
5
|
LICENSE
|
|
5
6
|
README.md
|
|
6
7
|
pyproject.toml
|
|
8
|
+
.github/workflows/changelog-pr-update.yml
|
|
9
|
+
.github/workflows/ci.yml
|
|
10
|
+
.github/workflows/label-public-pr.yml
|
|
7
11
|
.github/workflows/python-publish.yml
|
|
12
|
+
.github/workflows/readthedocs-pr-update.yml
|
|
8
13
|
docs/Makefile
|
|
9
14
|
docs/conf.py
|
|
10
15
|
docs/index.rst
|
|
@@ -46,4 +51,5 @@ src/anemoi_utils.egg-info/entry_points.txt
|
|
|
46
51
|
src/anemoi_utils.egg-info/requires.txt
|
|
47
52
|
src/anemoi_utils.egg-info/top_level.txt
|
|
48
53
|
tests/test_dates.py
|
|
54
|
+
tests/test_frequency.py
|
|
49
55
|
tests/test_utils.py
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# (C) Copyright 2023 European Centre for Medium-Range Weather Forecasts.
|
|
2
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
3
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
4
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
5
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
6
|
+
# nor does it submit to any jurisdiction.
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
|
|
10
|
+
from anemoi.utils.dates import frequency_to_string
|
|
11
|
+
from anemoi.utils.dates import frequency_to_timedelta
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_frequency_to_string():
|
|
15
|
+
assert frequency_to_string(datetime.timedelta(hours=1)) == "1h"
|
|
16
|
+
assert frequency_to_string(datetime.timedelta(hours=1, minutes=30)) == "1:30:00"
|
|
17
|
+
assert frequency_to_string(datetime.timedelta(days=10)) == "10d"
|
|
18
|
+
assert frequency_to_string(datetime.timedelta(minutes=10)) == "10m"
|
|
19
|
+
assert frequency_to_string(datetime.timedelta(minutes=90)) == "1:30:00"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_frequency_to_timedelta():
|
|
23
|
+
assert frequency_to_timedelta("1s") == datetime.timedelta(seconds=1)
|
|
24
|
+
assert frequency_to_timedelta("3m") == datetime.timedelta(minutes=3)
|
|
25
|
+
assert frequency_to_timedelta("1h") == datetime.timedelta(hours=1)
|
|
26
|
+
assert frequency_to_timedelta("3d") == datetime.timedelta(days=3)
|
|
27
|
+
assert frequency_to_timedelta("90m") == datetime.timedelta(hours=1, minutes=30)
|
|
28
|
+
assert frequency_to_timedelta("0:30") == datetime.timedelta(minutes=30)
|
|
29
|
+
assert frequency_to_timedelta("0:30:10") == datetime.timedelta(minutes=30, seconds=10)
|
|
30
|
+
assert frequency_to_timedelta("1:30:10") == datetime.timedelta(hours=1, minutes=30, seconds=10)
|
|
31
|
+
|
|
32
|
+
assert frequency_to_timedelta("PT10M") == datetime.timedelta(minutes=10)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
test_frequency_to_string()
|
|
37
|
+
test_frequency_to_timedelta()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|