anemoi-utils 0.3.12__tar.gz → 0.3.14__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.14/.github/workflows/changelog-pr-update.yml +15 -0
- anemoi_utils-0.3.14/.github/workflows/ci.yml +40 -0
- anemoi_utils-0.3.14/.github/workflows/label-public-pr.yml +10 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.github/workflows/python-publish.yml +3 -23
- anemoi_utils-0.3.14/.github/workflows/readthedocs-pr-update.yml +22 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.readthedocs.yaml +0 -1
- anemoi_utils-0.3.14/CHANGELOG.md +39 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/PKG-INFO +4 -5
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/pyproject.toml +6 -6
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/_version.py +2 -2
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/cli.py +7 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/dates.py +119 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/humanize.py +1 -1
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/s3.py +2 -1
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/text.py +85 -11
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/timer.py +2 -2
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/PKG-INFO +4 -5
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/SOURCES.txt +5 -1
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/requires.txt +3 -4
- anemoi_utils-0.3.12/docs/requirements.txt +0 -10
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.gitignore +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.pre-commit-config.yaml +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/LICENSE +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/README.md +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/Makefile +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/_static/style.css +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/conf.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/index.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/installing.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/setup.cfg +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/caching.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/checkpoints.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/config.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/provenance.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/tests/test_dates.py +0 -0
- {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/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.14
|
|
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
|
|
@@ -239,19 +240,17 @@ Requires-Dist: pandoc; extra == "dev"
|
|
|
239
240
|
Requires-Dist: pytest; extra == "dev"
|
|
240
241
|
Requires-Dist: requests; extra == "dev"
|
|
241
242
|
Requires-Dist: sphinx; extra == "dev"
|
|
242
|
-
Requires-Dist: sphinx-argparse; extra == "dev"
|
|
243
|
+
Requires-Dist: sphinx-argparse<0.5; extra == "dev"
|
|
243
244
|
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
|
244
245
|
Requires-Dist: termcolor; extra == "dev"
|
|
245
|
-
Requires-Dist: tomli; extra == "dev"
|
|
246
246
|
Provides-Extra: docs
|
|
247
247
|
Requires-Dist: nbsphinx; extra == "docs"
|
|
248
248
|
Requires-Dist: pandoc; extra == "docs"
|
|
249
249
|
Requires-Dist: requests; extra == "docs"
|
|
250
250
|
Requires-Dist: sphinx; extra == "docs"
|
|
251
|
-
Requires-Dist: sphinx-argparse; extra == "docs"
|
|
251
|
+
Requires-Dist: sphinx-argparse<0.5; extra == "docs"
|
|
252
252
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
253
253
|
Requires-Dist: termcolor; extra == "docs"
|
|
254
|
-
Requires-Dist: tomli; extra == "docs"
|
|
255
254
|
Provides-Extra: grib
|
|
256
255
|
Requires-Dist: requests; extra == "grib"
|
|
257
256
|
Provides-Extra: provenance
|
|
@@ -49,6 +49,8 @@ dynamic = [
|
|
|
49
49
|
"version",
|
|
50
50
|
]
|
|
51
51
|
dependencies = [
|
|
52
|
+
"isodate",
|
|
53
|
+
|
|
52
54
|
"pyyaml",
|
|
53
55
|
"tomli", # Only needed before 3.11
|
|
54
56
|
"tqdm",
|
|
@@ -68,23 +70,21 @@ optional-dependencies.dev = [
|
|
|
68
70
|
"pytest",
|
|
69
71
|
"requests",
|
|
70
72
|
"sphinx",
|
|
71
|
-
"sphinx-argparse",
|
|
73
|
+
"sphinx-argparse<0.5",
|
|
72
74
|
"sphinx-rtd-theme",
|
|
73
75
|
"termcolor",
|
|
74
|
-
"tomli",
|
|
75
76
|
]
|
|
76
|
-
|
|
77
|
-
# `pip install .[docs]`
|
|
77
|
+
|
|
78
78
|
optional-dependencies.docs = [
|
|
79
79
|
"nbsphinx",
|
|
80
80
|
"pandoc",
|
|
81
81
|
"requests",
|
|
82
82
|
"sphinx",
|
|
83
|
-
"sphinx-argparse",
|
|
83
|
+
"sphinx-argparse<0.5",
|
|
84
84
|
"sphinx-rtd-theme",
|
|
85
85
|
"termcolor",
|
|
86
|
-
"tomli",
|
|
87
86
|
]
|
|
87
|
+
|
|
88
88
|
optional-dependencies.grib = [
|
|
89
89
|
"requests",
|
|
90
90
|
]
|
|
@@ -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,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import calendar
|
|
10
10
|
import datetime
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import isodate
|
|
11
14
|
|
|
12
15
|
from .hindcasts import HindcastDatesTimes
|
|
13
16
|
|
|
@@ -66,6 +69,122 @@ def as_datetime(date):
|
|
|
66
69
|
raise ValueError(f"Invalid date type: {type(date)}")
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
def _compress_dates(dates):
|
|
73
|
+
dates = sorted(dates)
|
|
74
|
+
if len(dates) < 3:
|
|
75
|
+
yield dates
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
prev = first = dates.pop(0)
|
|
79
|
+
curr = dates.pop(0)
|
|
80
|
+
delta = curr - prev
|
|
81
|
+
while curr - prev == delta:
|
|
82
|
+
prev = curr
|
|
83
|
+
if not dates:
|
|
84
|
+
break
|
|
85
|
+
curr = dates.pop(0)
|
|
86
|
+
|
|
87
|
+
yield (first, prev, delta)
|
|
88
|
+
if dates:
|
|
89
|
+
yield from _compress_dates([curr] + dates)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def compress_dates(dates):
|
|
93
|
+
dates = [as_datetime(_) for _ in dates]
|
|
94
|
+
result = []
|
|
95
|
+
|
|
96
|
+
for n in _compress_dates(dates):
|
|
97
|
+
if isinstance(n, list):
|
|
98
|
+
result.extend([str(_) for _ in n])
|
|
99
|
+
else:
|
|
100
|
+
result.append(" ".join([str(n[0]), "to", str(n[1]), "by", str(n[2])]))
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def print_dates(dates):
|
|
106
|
+
print(compress_dates(dates))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def frequency_to_string(frequency):
|
|
110
|
+
# TODO: use iso8601
|
|
111
|
+
frequency = frequency_to_timedelta(frequency)
|
|
112
|
+
|
|
113
|
+
total_seconds = frequency.total_seconds()
|
|
114
|
+
assert int(total_seconds) == total_seconds, total_seconds
|
|
115
|
+
total_seconds = int(total_seconds)
|
|
116
|
+
|
|
117
|
+
seconds = total_seconds
|
|
118
|
+
|
|
119
|
+
days = seconds // (24 * 3600)
|
|
120
|
+
seconds %= 24 * 3600
|
|
121
|
+
hours = seconds // 3600
|
|
122
|
+
seconds %= 3600
|
|
123
|
+
minutes = seconds // 60
|
|
124
|
+
seconds %= 60
|
|
125
|
+
|
|
126
|
+
if days > 0 and hours == 0 and minutes == 0 and seconds == 0:
|
|
127
|
+
return f"{days}d"
|
|
128
|
+
|
|
129
|
+
if days == 0 and hours > 0 and minutes == 0 and seconds == 0:
|
|
130
|
+
return f"{hours}h"
|
|
131
|
+
|
|
132
|
+
if days == 0 and hours == 0 and minutes > 0 and seconds == 0:
|
|
133
|
+
return f"{minutes}m"
|
|
134
|
+
|
|
135
|
+
if days == 0 and hours == 0 and minutes == 0 and seconds > 0:
|
|
136
|
+
return f"{seconds}s"
|
|
137
|
+
|
|
138
|
+
if days > 0:
|
|
139
|
+
return f"{total_seconds}s"
|
|
140
|
+
|
|
141
|
+
return str(frequency)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def frequency_to_timedelta(frequency):
|
|
145
|
+
# TODO: use iso8601 or check pytimeparse
|
|
146
|
+
|
|
147
|
+
if isinstance(frequency, datetime.timedelta):
|
|
148
|
+
return frequency
|
|
149
|
+
|
|
150
|
+
if isinstance(frequency, int):
|
|
151
|
+
return datetime.timedelta(hours=frequency)
|
|
152
|
+
|
|
153
|
+
assert isinstance(frequency, str), (type(frequency), frequency)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
return frequency_to_timedelta(int(frequency))
|
|
157
|
+
except ValueError:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
if re.match(r"^\d+[hdms]$", frequency, re.IGNORECASE):
|
|
161
|
+
unit = frequency[-1].lower()
|
|
162
|
+
v = int(frequency[:-1])
|
|
163
|
+
unit = {"h": "hours", "d": "days", "s": "seconds", "m": "minutes"}[unit]
|
|
164
|
+
return datetime.timedelta(**{unit: v})
|
|
165
|
+
|
|
166
|
+
m = frequency.split(":")
|
|
167
|
+
if len(m) == 2:
|
|
168
|
+
return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]))
|
|
169
|
+
|
|
170
|
+
if len(m) == 3:
|
|
171
|
+
return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]), seconds=int(m[2]))
|
|
172
|
+
|
|
173
|
+
# ISO8601
|
|
174
|
+
try:
|
|
175
|
+
return isodate.parse_duration(frequency)
|
|
176
|
+
except isodate.isoerror.ISO8601Error:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
raise ValueError(f"Cannot convert frequency {frequency} to timedelta")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def normalize_date(x):
|
|
183
|
+
if isinstance(x, str):
|
|
184
|
+
return no_time_zone(datetime.datetime.fromisoformat(x))
|
|
185
|
+
return x
|
|
186
|
+
|
|
187
|
+
|
|
69
188
|
DOW = {
|
|
70
189
|
"monday": 0,
|
|
71
190
|
"tuesday": 1,
|
|
@@ -268,7 +268,7 @@ class Download(Transfer):
|
|
|
268
268
|
def source_size(self, s3_object):
|
|
269
269
|
return s3_object["Size"]
|
|
270
270
|
|
|
271
|
-
def transfer_file(self, source, target, overwrite, resume, verbosity, progress, config=None):
|
|
271
|
+
def transfer_file(self, source, target, overwrite, resume, verbosity, progress=None, config=None):
|
|
272
272
|
try:
|
|
273
273
|
return self._transfer_file(source, target, overwrite, resume, verbosity, config=config)
|
|
274
274
|
except Exception as e:
|
|
@@ -343,6 +343,7 @@ def upload(source, target, *, overwrite=False, resume=False, verbosity=1, progre
|
|
|
343
343
|
"""
|
|
344
344
|
|
|
345
345
|
uploader = Upload()
|
|
346
|
+
|
|
346
347
|
if os.path.isdir(source):
|
|
347
348
|
uploader.transfer_folder(
|
|
348
349
|
source=source,
|
|
@@ -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
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import logging
|
|
11
11
|
import time
|
|
12
12
|
|
|
13
|
-
from .humanize import
|
|
13
|
+
from .humanize import seconds_to_human
|
|
14
14
|
|
|
15
15
|
LOGGER = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -31,4 +31,4 @@ class Timer:
|
|
|
31
31
|
return time.time() - self.start
|
|
32
32
|
|
|
33
33
|
def __exit__(self, *args):
|
|
34
|
-
self.logger.info("%s: %s.", self.title,
|
|
34
|
+
self.logger.info("%s: %s.", self.title, seconds_to_human(self.elapsed))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.14
|
|
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
|
|
@@ -239,19 +240,17 @@ Requires-Dist: pandoc; extra == "dev"
|
|
|
239
240
|
Requires-Dist: pytest; extra == "dev"
|
|
240
241
|
Requires-Dist: requests; extra == "dev"
|
|
241
242
|
Requires-Dist: sphinx; extra == "dev"
|
|
242
|
-
Requires-Dist: sphinx-argparse; extra == "dev"
|
|
243
|
+
Requires-Dist: sphinx-argparse<0.5; extra == "dev"
|
|
243
244
|
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
|
244
245
|
Requires-Dist: termcolor; extra == "dev"
|
|
245
|
-
Requires-Dist: tomli; extra == "dev"
|
|
246
246
|
Provides-Extra: docs
|
|
247
247
|
Requires-Dist: nbsphinx; extra == "docs"
|
|
248
248
|
Requires-Dist: pandoc; extra == "docs"
|
|
249
249
|
Requires-Dist: requests; extra == "docs"
|
|
250
250
|
Requires-Dist: sphinx; extra == "docs"
|
|
251
|
-
Requires-Dist: sphinx-argparse; extra == "docs"
|
|
251
|
+
Requires-Dist: sphinx-argparse<0.5; extra == "docs"
|
|
252
252
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
253
253
|
Requires-Dist: termcolor; extra == "docs"
|
|
254
|
-
Requires-Dist: tomli; extra == "docs"
|
|
255
254
|
Provides-Extra: grib
|
|
256
255
|
Requires-Dist: requests; extra == "grib"
|
|
257
256
|
Provides-Extra: provenance
|
|
@@ -1,15 +1,19 @@
|
|
|
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
|
|
11
16
|
docs/installing.rst
|
|
12
|
-
docs/requirements.txt
|
|
13
17
|
docs/_static/logo.png
|
|
14
18
|
docs/_static/style.css
|
|
15
19
|
docs/_templates/.gitkeep
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
isodate
|
|
1
2
|
pyyaml
|
|
2
3
|
tomli
|
|
3
4
|
tqdm
|
|
@@ -16,20 +17,18 @@ pandoc
|
|
|
16
17
|
pytest
|
|
17
18
|
requests
|
|
18
19
|
sphinx
|
|
19
|
-
sphinx-argparse
|
|
20
|
+
sphinx-argparse<0.5
|
|
20
21
|
sphinx-rtd-theme
|
|
21
22
|
termcolor
|
|
22
|
-
tomli
|
|
23
23
|
|
|
24
24
|
[docs]
|
|
25
25
|
nbsphinx
|
|
26
26
|
pandoc
|
|
27
27
|
requests
|
|
28
28
|
sphinx
|
|
29
|
-
sphinx-argparse
|
|
29
|
+
sphinx-argparse<0.5
|
|
30
30
|
sphinx-rtd-theme
|
|
31
31
|
termcolor
|
|
32
|
-
tomli
|
|
33
32
|
|
|
34
33
|
[grib]
|
|
35
34
|
requests
|
|
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
|