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.
Files changed (40) hide show
  1. model_munger-0.3.1/.github/workflows/docker.yml +50 -0
  2. model_munger-0.3.1/.github/workflows/publish.yml +36 -0
  3. model_munger-0.3.1/.github/workflows/test.yml +30 -0
  4. model_munger-0.3.1/.gitignore +3 -0
  5. model_munger-0.3.1/.pre-commit-config.yaml +46 -0
  6. model_munger-0.3.1/CHANGELOG.md +55 -0
  7. model_munger-0.3.1/Dockerfile +6 -0
  8. model_munger-0.3.1/LICENSE +21 -0
  9. model_munger-0.3.1/PKG-INFO +62 -0
  10. model_munger-0.3.1/README.md +34 -0
  11. model_munger-0.3.1/_typos.toml +2 -0
  12. model_munger-0.3.1/pyproject.toml +71 -0
  13. model_munger-0.3.1/src/model_munger/__init__.py +0 -0
  14. model_munger-0.3.1/src/model_munger/cli.py +212 -0
  15. model_munger-0.3.1/src/model_munger/cloudnet.py +72 -0
  16. model_munger-0.3.1/src/model_munger/download.py +96 -0
  17. model_munger-0.3.1/src/model_munger/extract.py +232 -0
  18. model_munger-0.3.1/src/model_munger/extractors/ecmwf_open.py +113 -0
  19. model_munger-0.3.1/src/model_munger/extractors/gdas1.py +229 -0
  20. model_munger-0.3.1/src/model_munger/grid.py +71 -0
  21. model_munger-0.3.1/src/model_munger/level.py +25 -0
  22. model_munger-0.3.1/src/model_munger/merge.py +65 -0
  23. model_munger-0.3.1/src/model_munger/metadata.py +926 -0
  24. model_munger-0.3.1/src/model_munger/model.py +254 -0
  25. model_munger-0.3.1/src/model_munger/py.typed +0 -0
  26. model_munger-0.3.1/src/model_munger/readers/__init__.py +3 -0
  27. model_munger-0.3.1/src/model_munger/readers/arpege.py +99 -0
  28. model_munger-0.3.1/src/model_munger/readers/ecmwf_open.py +99 -0
  29. model_munger-0.3.1/src/model_munger/readers/gdas1.py +129 -0
  30. model_munger-0.3.1/src/model_munger/utils.py +148 -0
  31. model_munger-0.3.1/src/model_munger/version.py +1 -0
  32. model_munger-0.3.1/tests/__init__.py +0 -0
  33. model_munger-0.3.1/tests/common.py +8 -0
  34. model_munger-0.3.1/tests/data/20250115000000-0h-oper-fc.grib2 +0 -0
  35. model_munger-0.3.1/tests/data/20250115000000-3h-oper-fc.grib2 +0 -0
  36. model_munger-0.3.1/tests/test_ecmwf_open.py +68 -0
  37. model_munger-0.3.1/tests/test_grid.py +19 -0
  38. model_munger-0.3.1/tests/test_merge.py +117 -0
  39. model_munger-0.3.1/tests/test_model.py +67 -0
  40. 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,3 @@
1
+ __pycache__
2
+ /data
3
+ /output
@@ -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,6 @@
1
+ FROM python:3.10-bullseye AS base
2
+
3
+ WORKDIR /app
4
+ COPY pyproject.toml README.md /app
5
+ COPY src /app/src
6
+ RUN pip3 install --upgrade pip && pip3 install --no-cache-dir .
@@ -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
+ [![Run tests](https://github.com/actris-cloudnet/model-munger/actions/workflows/test.yml/badge.svg)](https://github.com/actris-cloudnet/model-munger/actions/workflows/test.yml)
32
+ [![PyPI version](https://badge.fury.io/py/model-munger.svg)](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
+ [![Run tests](https://github.com/actris-cloudnet/model-munger/actions/workflows/test.yml/badge.svg)](https://github.com/actris-cloudnet/model-munger/actions/workflows/test.yml)
4
+ [![PyPI version](https://badge.fury.io/py/model-munger.svg)](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,2 @@
1
+ [default]
2
+ extend-ignore-identifiers-re = ["isobaricInhPa", "tke"]
@@ -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()