model-munger 0.3.1__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.
- model_munger-0.3.1/.github/workflows/docker.yml +50 -0
- model_munger-0.3.1/.github/workflows/publish.yml +36 -0
- model_munger-0.3.1/.github/workflows/test.yml +30 -0
- model_munger-0.3.1/.gitignore +3 -0
- model_munger-0.3.1/.pre-commit-config.yaml +46 -0
- model_munger-0.3.1/CHANGELOG.md +55 -0
- model_munger-0.3.1/Dockerfile +6 -0
- model_munger-0.3.1/LICENSE +21 -0
- model_munger-0.3.1/PKG-INFO +62 -0
- model_munger-0.3.1/README.md +34 -0
- model_munger-0.3.1/_typos.toml +2 -0
- model_munger-0.3.1/pyproject.toml +71 -0
- model_munger-0.3.1/src/model_munger/__init__.py +0 -0
- model_munger-0.3.1/src/model_munger/cli.py +212 -0
- model_munger-0.3.1/src/model_munger/cloudnet.py +72 -0
- model_munger-0.3.1/src/model_munger/download.py +96 -0
- model_munger-0.3.1/src/model_munger/extract.py +232 -0
- model_munger-0.3.1/src/model_munger/extractors/ecmwf_open.py +113 -0
- model_munger-0.3.1/src/model_munger/extractors/gdas1.py +229 -0
- model_munger-0.3.1/src/model_munger/grid.py +71 -0
- model_munger-0.3.1/src/model_munger/level.py +25 -0
- model_munger-0.3.1/src/model_munger/merge.py +65 -0
- model_munger-0.3.1/src/model_munger/metadata.py +926 -0
- model_munger-0.3.1/src/model_munger/model.py +254 -0
- model_munger-0.3.1/src/model_munger/py.typed +0 -0
- model_munger-0.3.1/src/model_munger/readers/__init__.py +3 -0
- model_munger-0.3.1/src/model_munger/readers/arpege.py +99 -0
- model_munger-0.3.1/src/model_munger/readers/ecmwf_open.py +99 -0
- model_munger-0.3.1/src/model_munger/readers/gdas1.py +129 -0
- model_munger-0.3.1/src/model_munger/utils.py +148 -0
- model_munger-0.3.1/src/model_munger/version.py +1 -0
- model_munger-0.3.1/tests/__init__.py +0 -0
- model_munger-0.3.1/tests/common.py +8 -0
- model_munger-0.3.1/tests/data/20250115000000-0h-oper-fc.grib2 +0 -0
- model_munger-0.3.1/tests/data/20250115000000-3h-oper-fc.grib2 +0 -0
- model_munger-0.3.1/tests/test_ecmwf_open.py +68 -0
- model_munger-0.3.1/tests/test_grid.py +19 -0
- model_munger-0.3.1/tests/test_merge.py +117 -0
- model_munger-0.3.1/tests/test_model.py +67 -0
- model_munger-0.3.1/tests/test_utils.py +52 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Create and publish a Docker image
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
env:
|
|
6
|
+
REGISTRY: ghcr.io
|
|
7
|
+
IMAGE_NAME: ${{ github.repository }}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-and-push-image:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
packages: write
|
|
15
|
+
attestations: write
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Log in to the Container registry
|
|
22
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
23
|
+
uses: docker/login-action@v3
|
|
24
|
+
with:
|
|
25
|
+
registry: ${{ env.REGISTRY }}
|
|
26
|
+
username: ${{ github.actor }}
|
|
27
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
+
|
|
29
|
+
- name: Extract metadata (tags, labels) for Docker
|
|
30
|
+
id: meta
|
|
31
|
+
uses: docker/metadata-action@v5
|
|
32
|
+
with:
|
|
33
|
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
34
|
+
|
|
35
|
+
- name: Build and push Docker image
|
|
36
|
+
id: push
|
|
37
|
+
uses: docker/build-push-action@v6
|
|
38
|
+
with:
|
|
39
|
+
context: .
|
|
40
|
+
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
41
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
42
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
43
|
+
|
|
44
|
+
- name: Generate artifact attestation
|
|
45
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
46
|
+
uses: actions/attest-build-provenance@v1
|
|
47
|
+
with:
|
|
48
|
+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
|
49
|
+
subject-digest: ${{ steps.push.outputs.digest }}
|
|
50
|
+
push-to-registry: true
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Upload Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.10"
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: python -m pip install build
|
|
22
|
+
- name: Build package
|
|
23
|
+
run: python -m build
|
|
24
|
+
- name: Publish package
|
|
25
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
26
|
+
- name: Generate changelog
|
|
27
|
+
run: |
|
|
28
|
+
version=${GITHUB_REF#refs/tags/v}
|
|
29
|
+
sed "0,/^## ${version//./\\.}/d;/^## /,\$d" CHANGELOG.md > ${{ github.workspace }}-CHANGELOG.txt
|
|
30
|
+
echo "name=Model Munger $version" >> $GITHUB_OUTPUT
|
|
31
|
+
id: changelog
|
|
32
|
+
- name: Create release
|
|
33
|
+
uses: softprops/action-gh-release@v1
|
|
34
|
+
with:
|
|
35
|
+
name: ${{ steps.changelog.outputs.name }}
|
|
36
|
+
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Run tests
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
strategy:
|
|
8
|
+
matrix:
|
|
9
|
+
os: [ubuntu-latest]
|
|
10
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
11
|
+
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
cache: "pip"
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: |
|
|
22
|
+
pip install --upgrade pip
|
|
23
|
+
pip install .[dev,test]
|
|
24
|
+
- name: Run pre-commit checks
|
|
25
|
+
if: startsWith(matrix.os, 'ubuntu-')
|
|
26
|
+
run: |
|
|
27
|
+
pre-commit run --all-files --show-diff-on-failure
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: |
|
|
30
|
+
pytest
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: check-case-conflict
|
|
6
|
+
- id: check-executables-have-shebangs
|
|
7
|
+
- id: check-merge-conflict
|
|
8
|
+
- id: check-shebang-scripts-are-executable
|
|
9
|
+
- id: end-of-file-fixer
|
|
10
|
+
exclude: ^tests/unit/data/
|
|
11
|
+
- id: fix-byte-order-marker
|
|
12
|
+
exclude: ^tests/unit/data/
|
|
13
|
+
- id: mixed-line-ending
|
|
14
|
+
args: ["--fix", "lf"]
|
|
15
|
+
exclude: ^tests/unit/data/
|
|
16
|
+
- id: requirements-txt-fixer
|
|
17
|
+
- id: trailing-whitespace
|
|
18
|
+
exclude: ^tests/unit/data/
|
|
19
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
20
|
+
rev: v0.9.7
|
|
21
|
+
hooks:
|
|
22
|
+
- id: ruff
|
|
23
|
+
args: ["--fix"]
|
|
24
|
+
- id: ruff-format
|
|
25
|
+
- repo: local
|
|
26
|
+
hooks:
|
|
27
|
+
- id: mypy
|
|
28
|
+
name: mypy
|
|
29
|
+
entry: mypy
|
|
30
|
+
language: system
|
|
31
|
+
types: [python]
|
|
32
|
+
require_serial: true
|
|
33
|
+
- repo: https://github.com/pre-commit/mirrors-prettier
|
|
34
|
+
rev: v3.1.0
|
|
35
|
+
hooks:
|
|
36
|
+
- id: prettier
|
|
37
|
+
exclude: ^docs/source/_templates/
|
|
38
|
+
- repo: https://github.com/pappasam/toml-sort
|
|
39
|
+
rev: v0.24.2
|
|
40
|
+
hooks:
|
|
41
|
+
- id: toml-sort-fix
|
|
42
|
+
- repo: https://github.com/crate-ci/typos
|
|
43
|
+
rev: v1.29.10
|
|
44
|
+
hooks:
|
|
45
|
+
- id: typos
|
|
46
|
+
args: ["--force-exclude"]
|
|
@@ -0,0 +1,55 @@
|
|
|
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.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## 0.3.1 – 2025-08-20
|
|
9
|
+
|
|
10
|
+
- Add `sfc_geopotential` and `sfc_height` to `ecmwf-open`
|
|
11
|
+
|
|
12
|
+
## 0.3.0 – 2025-08-11
|
|
13
|
+
|
|
14
|
+
- Add GDAS1 support
|
|
15
|
+
- Add `source` attribute to netCDF output
|
|
16
|
+
- Add `comment` attribute to `rh` variable in netCDF output
|
|
17
|
+
|
|
18
|
+
## 0.2.0 – 2025-04-15
|
|
19
|
+
|
|
20
|
+
- Support moving sites
|
|
21
|
+
- Add `--steps` argument
|
|
22
|
+
|
|
23
|
+
## 0.1.5 – 2025-02-10
|
|
24
|
+
|
|
25
|
+
- Handle missing values in `ecmwf-open` extractor
|
|
26
|
+
|
|
27
|
+
## 0.1.4 – 2025-02-07
|
|
28
|
+
|
|
29
|
+
- Improve handling of missing variables when merging models
|
|
30
|
+
- Reduce logging for non-interactive sessions
|
|
31
|
+
|
|
32
|
+
## 0.1.3 – 2025-01-29
|
|
33
|
+
|
|
34
|
+
- Fix processing date ranges in CLI
|
|
35
|
+
|
|
36
|
+
## 0.1.2 – 2025-01-28
|
|
37
|
+
|
|
38
|
+
- Handle missing variables in `ecmwf-open`
|
|
39
|
+
|
|
40
|
+
## 0.1.1 – 2025-01-28
|
|
41
|
+
|
|
42
|
+
- Support "today" and "yesterday" values in CLI
|
|
43
|
+
|
|
44
|
+
## 0.1.0 – 2025-01-28
|
|
45
|
+
|
|
46
|
+
- Add ARPEGE support
|
|
47
|
+
- Add support for intermediate files
|
|
48
|
+
|
|
49
|
+
## 0.0.2 – 2024-10-07
|
|
50
|
+
|
|
51
|
+
- Fix password environment variable
|
|
52
|
+
|
|
53
|
+
## 0.0.1 – 2024-10-07
|
|
54
|
+
|
|
55
|
+
- Initial release with ECMWF open data support
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Finnish Meteorological Institute
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: model_munger
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Extract vertical profiles from NWP models and output netCDF files
|
|
5
|
+
Project-URL: Homepage, https://github.com/actris-cloudnet/model-munger
|
|
6
|
+
Project-URL: Issues, https://github.com/actris-cloudnet/model-munger/issues
|
|
7
|
+
Author-email: Tuomas Siipola <tuomas.siipola@fmi.fi>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: netcdf4
|
|
17
|
+
Requires-Dist: pygrib
|
|
18
|
+
Requires-Dist: requests
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
21
|
+
Requires-Dist: release-version; extra == 'dev'
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: mypy; extra == 'test'
|
|
24
|
+
Requires-Dist: pytest; extra == 'test'
|
|
25
|
+
Requires-Dist: ruff; extra == 'test'
|
|
26
|
+
Requires-Dist: types-requests; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Model Munger
|
|
30
|
+
|
|
31
|
+
[](https://github.com/actris-cloudnet/model-munger/actions/workflows/test.yml)
|
|
32
|
+
[](https://badge.fury.io/py/model-munger)
|
|
33
|
+
|
|
34
|
+
Extract vertical profiles from numerical weather prediction (NWP) models and
|
|
35
|
+
output netCDF files.
|
|
36
|
+
|
|
37
|
+
## Supported models
|
|
38
|
+
|
|
39
|
+
| Model | Horizontal resolution | Vertical resolution | Temporal resolution | Download |
|
|
40
|
+
| ------------------------------------------------------------------------ | --------------------- | ------------------- | ------------------- | ---------------------------------------- |
|
|
41
|
+
| [ARPEGE](https://www.umr-cnrm.fr/spip.php?article121&lang=en) | Native | 105 model levels | 1 hour | Not supported |
|
|
42
|
+
| [ECMWF open data](https://www.ecmwf.int/en/forecasts/datasets/open-data) | 0.25 degrees | 13 pressure levels | 3 hours | Last days from ECMWF, few years from AWS |
|
|
43
|
+
| [GDAS1](https://www.ready.noaa.gov/gdas1.php) | 1 degree | 23 pressure levels | 3 hours | Since December 2004 |
|
|
44
|
+
|
|
45
|
+
## Processing steps
|
|
46
|
+
|
|
47
|
+
Model Munger deals with three types of files:
|
|
48
|
+
|
|
49
|
+
- **Raw data** is model output stored for example as GRIB files. Model Munger
|
|
50
|
+
can download the raw data for some models.
|
|
51
|
+
- **Intermediate files** are netCDF files containing vertical profiles in a
|
|
52
|
+
single fixed or moving location. There may be multiple intermediate files, for
|
|
53
|
+
example one for each model run. For some models, Model Munger extracts
|
|
54
|
+
intermediate files from the raw data, but for other models, it uses the output
|
|
55
|
+
from other tools.
|
|
56
|
+
- **Output file** is a harmonized netCDF file generated from one or more
|
|
57
|
+
intermediate files. The output file contains up to 24 hours of data from a
|
|
58
|
+
single fixed or moving location, possibly combined from different model runs.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Model Munger
|
|
2
|
+
|
|
3
|
+
[](https://github.com/actris-cloudnet/model-munger/actions/workflows/test.yml)
|
|
4
|
+
[](https://badge.fury.io/py/model-munger)
|
|
5
|
+
|
|
6
|
+
Extract vertical profiles from numerical weather prediction (NWP) models and
|
|
7
|
+
output netCDF files.
|
|
8
|
+
|
|
9
|
+
## Supported models
|
|
10
|
+
|
|
11
|
+
| Model | Horizontal resolution | Vertical resolution | Temporal resolution | Download |
|
|
12
|
+
| ------------------------------------------------------------------------ | --------------------- | ------------------- | ------------------- | ---------------------------------------- |
|
|
13
|
+
| [ARPEGE](https://www.umr-cnrm.fr/spip.php?article121&lang=en) | Native | 105 model levels | 1 hour | Not supported |
|
|
14
|
+
| [ECMWF open data](https://www.ecmwf.int/en/forecasts/datasets/open-data) | 0.25 degrees | 13 pressure levels | 3 hours | Last days from ECMWF, few years from AWS |
|
|
15
|
+
| [GDAS1](https://www.ready.noaa.gov/gdas1.php) | 1 degree | 23 pressure levels | 3 hours | Since December 2004 |
|
|
16
|
+
|
|
17
|
+
## Processing steps
|
|
18
|
+
|
|
19
|
+
Model Munger deals with three types of files:
|
|
20
|
+
|
|
21
|
+
- **Raw data** is model output stored for example as GRIB files. Model Munger
|
|
22
|
+
can download the raw data for some models.
|
|
23
|
+
- **Intermediate files** are netCDF files containing vertical profiles in a
|
|
24
|
+
single fixed or moving location. There may be multiple intermediate files, for
|
|
25
|
+
example one for each model run. For some models, Model Munger extracts
|
|
26
|
+
intermediate files from the raw data, but for other models, it uses the output
|
|
27
|
+
from other tools.
|
|
28
|
+
- **Output file** is a harmonized netCDF file generated from one or more
|
|
29
|
+
intermediate files. The output file contains up to 24 hours of data from a
|
|
30
|
+
single fixed or moving location, possibly combined from different model runs.
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "model_munger"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Tuomas Siipola", email = "tuomas.siipola@fmi.fi"},
|
|
9
|
+
]
|
|
10
|
+
description = "Extract vertical profiles from NWP models and output netCDF files"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"netCDF4",
|
|
23
|
+
"pygrib",
|
|
24
|
+
"requests",
|
|
25
|
+
]
|
|
26
|
+
dynamic = ["version"]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = [
|
|
30
|
+
"mypy",
|
|
31
|
+
"pytest",
|
|
32
|
+
"ruff",
|
|
33
|
+
"types-requests",
|
|
34
|
+
]
|
|
35
|
+
dev = ["pre-commit", "release-version"]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
model-munger = "model_munger.cli:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/actris-cloudnet/model-munger"
|
|
42
|
+
Issues = "https://github.com/actris-cloudnet/model-munger/issues"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.version]
|
|
45
|
+
path = "src/model_munger/version.py"
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
check_untyped_defs = true
|
|
49
|
+
|
|
50
|
+
[[tool.mypy.overrides]]
|
|
51
|
+
module = ["cftime.*", "pygrib.*"]
|
|
52
|
+
ignore_missing_imports = true
|
|
53
|
+
|
|
54
|
+
[tool.release-version]
|
|
55
|
+
filename = "src/model_munger/version.py"
|
|
56
|
+
pattern = ["__version__ = \"(?P<major>\\d+).(?P<minor>\\d+).(?P<patch>\\d+)\""]
|
|
57
|
+
changelog = "CHANGELOG.md"
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = ["B", "D", "E", "F", "I", "PLC", "PLE", "PLW", "SIM", "UP"]
|
|
61
|
+
ignore = ["D1"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.per-file-ignores]
|
|
64
|
+
"__init__.py" = ["PLC0414"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.pydocstyle]
|
|
67
|
+
convention = "google"
|
|
68
|
+
|
|
69
|
+
[tool.tomlsort]
|
|
70
|
+
trailing_comma_inline_array = true
|
|
71
|
+
sort_inline_arrays = true
|
|
File without changes
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from model_munger.cloudnet import get_locations, get_sites, submit_file
|
|
7
|
+
from model_munger.download import download_file
|
|
8
|
+
from model_munger.extract import RawLocation, extract_profiles, write_netcdf
|
|
9
|
+
from model_munger.extractors.ecmwf_open import generate_ecmwf_url, read_ecmwf
|
|
10
|
+
from model_munger.extractors.gdas1 import generate_gdas1_url, read_gdas1
|
|
11
|
+
from model_munger.level import Level
|
|
12
|
+
from model_munger.readers.ecmwf_open import ECMWF_OPEN
|
|
13
|
+
from model_munger.readers.gdas1 import GDAS1
|
|
14
|
+
from model_munger.version import __version__ as model_munger_version
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
parser = argparse.ArgumentParser()
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"-d",
|
|
21
|
+
"--date",
|
|
22
|
+
type=parse_date,
|
|
23
|
+
help="Fetch ECMWF open data for this date. Default is today.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--start",
|
|
27
|
+
type=parse_date,
|
|
28
|
+
help="Fetch ECMWF open data starting from this date. Default is today.",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--stop",
|
|
32
|
+
type=parse_date,
|
|
33
|
+
help="Fetch ECMWF open data until this date. Default is today.",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-r",
|
|
37
|
+
"--runs",
|
|
38
|
+
type=lambda x: [int(y) for y in x.split(",")],
|
|
39
|
+
default=[0],
|
|
40
|
+
help="Comma-separated list of model runs to download.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--steps",
|
|
44
|
+
type=int,
|
|
45
|
+
default=90,
|
|
46
|
+
help="Maximum time step. Default is 90 hours.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"-s",
|
|
50
|
+
"--sites",
|
|
51
|
+
type=lambda x: x.split(","),
|
|
52
|
+
help="Comma-separated list of Cloudnet sites (e.g. hyytiala) to extract.",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"-m",
|
|
56
|
+
"--model",
|
|
57
|
+
choices=["ecmwf-open", "gdas1"],
|
|
58
|
+
help="Which model to download and process.",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--source",
|
|
62
|
+
choices=["ecmwf", "noaa", "aws"],
|
|
63
|
+
help="Where to download ECMWF open data from.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--submit",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Submit files to Cloudnet.",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--no-keep",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Don't keep downloaded and processed files.",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
args = parser.parse_args()
|
|
77
|
+
|
|
78
|
+
if args.date and (args.start or args.stop):
|
|
79
|
+
parser.error("Cannot use --date with --start and --stop")
|
|
80
|
+
if args.date:
|
|
81
|
+
args.start = args.date
|
|
82
|
+
args.stop = args.date
|
|
83
|
+
else:
|
|
84
|
+
if not args.start:
|
|
85
|
+
args.start = utctoday()
|
|
86
|
+
if not args.stop:
|
|
87
|
+
args.stop = utctoday()
|
|
88
|
+
if args.start > args.stop:
|
|
89
|
+
parser.error("--start should be before --stop")
|
|
90
|
+
del args.date
|
|
91
|
+
|
|
92
|
+
if args.sites:
|
|
93
|
+
all_sites = get_sites()
|
|
94
|
+
if invalid_sites := set(args.sites) - {site["id"] for site in all_sites}:
|
|
95
|
+
parser.error("Invalid sites: " + ",".join(invalid_sites))
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
sites = [site for site in all_sites if site["id"] in args.sites]
|
|
98
|
+
else:
|
|
99
|
+
sites = get_sites("cloudnet")
|
|
100
|
+
|
|
101
|
+
download_dir = Path("data")
|
|
102
|
+
output_dir = Path("output")
|
|
103
|
+
download_dir.mkdir(exist_ok=True)
|
|
104
|
+
output_dir.mkdir(exist_ok=True)
|
|
105
|
+
|
|
106
|
+
current_files: set[Path] = set()
|
|
107
|
+
last_files: set[Path] = set()
|
|
108
|
+
|
|
109
|
+
def _remove_unused_files():
|
|
110
|
+
if args.no_keep:
|
|
111
|
+
unused_files = last_files - current_files
|
|
112
|
+
for file in unused_files:
|
|
113
|
+
print("Remove", file)
|
|
114
|
+
file.unlink()
|
|
115
|
+
last_files.clear()
|
|
116
|
+
last_files.update(current_files)
|
|
117
|
+
current_files.clear()
|
|
118
|
+
|
|
119
|
+
date = args.start
|
|
120
|
+
while date <= args.stop:
|
|
121
|
+
for run in args.runs:
|
|
122
|
+
locations = []
|
|
123
|
+
for site in sites:
|
|
124
|
+
if "mobile" in site["type"]:
|
|
125
|
+
one_day = datetime.timedelta(days=1)
|
|
126
|
+
time_prev, lat_prev, lon_prev = get_locations(
|
|
127
|
+
site["id"], date - one_day
|
|
128
|
+
)
|
|
129
|
+
time_curr, lat_curr, lon_curr = get_locations(site["id"], date)
|
|
130
|
+
time_next, lat_next, lon_next = get_locations(
|
|
131
|
+
site["id"], date + one_day
|
|
132
|
+
)
|
|
133
|
+
time = time_prev + time_curr + time_next
|
|
134
|
+
latitude = lat_prev + lat_curr + lat_next
|
|
135
|
+
longitude = lon_prev + lon_curr + lon_next
|
|
136
|
+
else:
|
|
137
|
+
time = None
|
|
138
|
+
latitude = site["latitude"]
|
|
139
|
+
longitude = site["longitude"]
|
|
140
|
+
locations.append(
|
|
141
|
+
RawLocation(
|
|
142
|
+
id=site["id"],
|
|
143
|
+
name=site["humanReadableName"],
|
|
144
|
+
time=time,
|
|
145
|
+
latitude=latitude,
|
|
146
|
+
longitude=longitude,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
levels: list[Level] = []
|
|
151
|
+
|
|
152
|
+
if args.model == "ecmwf-open":
|
|
153
|
+
model = ECMWF_OPEN
|
|
154
|
+
history = f"Model run {run:02} UTC extracted from ECMWF open data"
|
|
155
|
+
date_id = f"{date:%Y%m%d}{run:02}0000"
|
|
156
|
+
source = args.source or "ecmwf"
|
|
157
|
+
for step in range(0, args.steps + 1, 3):
|
|
158
|
+
url = generate_ecmwf_url(date, run, step, source)
|
|
159
|
+
path = download_file(url, download_dir)
|
|
160
|
+
levels.extend(read_ecmwf(path))
|
|
161
|
+
current_files.add(path)
|
|
162
|
+
elif args.model == "gdas1":
|
|
163
|
+
model = GDAS1
|
|
164
|
+
source = args.source or "noaa"
|
|
165
|
+
url, revalidate = generate_gdas1_url(date, source)
|
|
166
|
+
filename = url.rsplit("/", maxsplit=1)[-1]
|
|
167
|
+
history = f"GDAS1 data on {date:%Y-%m-%d} extracted from {filename}"
|
|
168
|
+
date_id = f"{date:%Y%m%d}"
|
|
169
|
+
path = download_file(url, download_dir, revalidate=revalidate)
|
|
170
|
+
for level in read_gdas1(path):
|
|
171
|
+
if level.time.date() < date:
|
|
172
|
+
continue
|
|
173
|
+
if level.time.date() > date:
|
|
174
|
+
break
|
|
175
|
+
levels.append(level)
|
|
176
|
+
current_files.add(path)
|
|
177
|
+
|
|
178
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
179
|
+
history_line = (
|
|
180
|
+
f"{now:%Y-%m-%d %H:%M:%S} +00:00 - {history} "
|
|
181
|
+
f"using model-munger v{model_munger_version}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
for raw in extract_profiles(levels, locations, model, history_line):
|
|
185
|
+
outfile = f"{date_id}_{raw.location.id}_{raw.model.id}.nc"
|
|
186
|
+
outpath = output_dir / outfile
|
|
187
|
+
print(outpath)
|
|
188
|
+
write_netcdf(raw, outpath)
|
|
189
|
+
if args.submit:
|
|
190
|
+
submit_file(outpath, raw.location, date, raw.model)
|
|
191
|
+
|
|
192
|
+
_remove_unused_files()
|
|
193
|
+
|
|
194
|
+
date += datetime.timedelta(days=1)
|
|
195
|
+
|
|
196
|
+
_remove_unused_files()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def utctoday():
|
|
200
|
+
return datetime.datetime.now(datetime.timezone.utc).date()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def parse_date(value: str) -> datetime.date:
|
|
204
|
+
if value == "today":
|
|
205
|
+
return utctoday()
|
|
206
|
+
if value == "yesterday":
|
|
207
|
+
return utctoday() - datetime.timedelta(days=1)
|
|
208
|
+
return datetime.date.fromisoformat(value)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
main()
|