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.

Files changed (57) hide show
  1. anemoi_utils-0.3.14/.github/workflows/changelog-pr-update.yml +15 -0
  2. anemoi_utils-0.3.14/.github/workflows/ci.yml +40 -0
  3. anemoi_utils-0.3.14/.github/workflows/label-public-pr.yml +10 -0
  4. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.github/workflows/python-publish.yml +3 -23
  5. anemoi_utils-0.3.14/.github/workflows/readthedocs-pr-update.yml +22 -0
  6. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.readthedocs.yaml +0 -1
  7. anemoi_utils-0.3.14/CHANGELOG.md +39 -0
  8. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/PKG-INFO +4 -5
  9. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/pyproject.toml +6 -6
  10. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/_version.py +2 -2
  11. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/cli.py +7 -0
  12. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/dates.py +119 -0
  13. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/humanize.py +1 -1
  14. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/s3.py +2 -1
  15. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/text.py +85 -11
  16. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/timer.py +2 -2
  17. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/PKG-INFO +4 -5
  18. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/SOURCES.txt +5 -1
  19. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/requires.txt +3 -4
  20. anemoi_utils-0.3.12/docs/requirements.txt +0 -10
  21. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.gitignore +0 -0
  22. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/.pre-commit-config.yaml +0 -0
  23. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/LICENSE +0 -0
  24. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/README.md +0 -0
  25. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/Makefile +0 -0
  26. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/_static/logo.png +0 -0
  27. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/_static/style.css +0 -0
  28. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/_templates/.gitkeep +0 -0
  29. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/conf.py +0 -0
  30. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/index.rst +0 -0
  31. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/installing.rst +0 -0
  32. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/checkpoints.rst +0 -0
  33. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/config.rst +0 -0
  34. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/dates.rst +0 -0
  35. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/grib.rst +0 -0
  36. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/humanize.rst +0 -0
  37. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/provenance.rst +0 -0
  38. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/s3.rst +0 -0
  39. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/docs/modules/text.rst +0 -0
  40. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/setup.cfg +0 -0
  41. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/__init__.py +0 -0
  42. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/__main__.py +0 -0
  43. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/caching.py +0 -0
  44. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/checkpoints.py +0 -0
  45. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/commands/__init__.py +0 -0
  46. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/commands/config.py +0 -0
  47. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/config.py +0 -0
  48. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/grib.py +0 -0
  49. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/hindcasts.py +0 -0
  50. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/mars/__init__.py +0 -0
  51. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/mars/mars.yaml +0 -0
  52. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi/utils/provenance.py +0 -0
  53. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
  54. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
  55. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/src/anemoi_utils.egg-info/top_level.txt +0 -0
  56. {anemoi_utils-0.3.12 → anemoi_utils-0.3.14}/tests/test_dates.py +0 -0
  57. {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
@@ -0,0 +1,10 @@
1
+ # Manage labels of pull requests that originate from forks
2
+ name: label-public-pr
3
+
4
+ on:
5
+ pull_request_target:
6
+ types: [opened, synchronize]
7
+
8
+ jobs:
9
+ label:
10
+ uses: ecmwf-actions/reusable-workflows/.github/workflows/label-pr.yml@v2
@@ -35,7 +35,7 @@ jobs:
35
35
  steps:
36
36
  - uses: actions/checkout@v4
37
37
 
38
- - uses: actions/setup-python@v2
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
- steps:
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"
@@ -10,7 +10,6 @@ sphinx:
10
10
 
11
11
  python:
12
12
  install:
13
- - requirements: docs/requirements.txt
14
13
  - method: pip
15
14
  path: .
16
15
  extra_requirements:
@@ -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.12
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
- # Loaded by read-the-docs
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,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3.12'
16
- __version_tuple__ = version_tuple = (0, 3, 12)
15
+ __version__ = version = '0.3.14'
16
+ __version_tuple__ = version_tuple = (0, 3, 14)
@@ -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,
@@ -104,7 +104,7 @@ def _plural(count):
104
104
  def seconds_to_human(seconds: float) -> str:
105
105
  """Convert a number of seconds to a human readable string
106
106
 
107
- >>> seconds(4000)
107
+ >>> seconds_to_human(4000)
108
108
  '1 hour 6 minutes 40 seconds'
109
109
 
110
110
  Parameters
@@ -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 = text.split("\n")
61
- width = max(len(_) for _ in lines)
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
- for line in text.split("\n"):
70
- if len(line) > max_width:
71
- line = line[: max_width - 1] + "…"
72
- lines.append(line)
73
- text = "\n".join(lines)
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
- box.append(f"│ {line:{width}} │")
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 seconds
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, seconds(self.elapsed))
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.12
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
@@ -1,10 +0,0 @@
1
- # These are the requirements for readthedoc
2
- sphinx
3
- sphinx_rtd_theme
4
- nbsphinx
5
- sphinx_argparse
6
-
7
- # Also requires `brew install pandoc` on Mac
8
- pandoc
9
-
10
- rstfmt
File without changes
File without changes
File without changes
File without changes