dwdown
dwdown is a Python package for downloading weather forecast and historical observation data from the Deutscher Wetterdienst (DWD) open data server, processing GRIB2 files, and uploading the results to S3-compatible object storage such as MinIO. The package also provides parallel transfers, post-download verification (file size for HTTP downloads, ETag for object storage), and status notifications.
The source code is available in the GitHub repository.
Features
- ForecastDownloader: downloads NWP forecast files (ICON, ICON-D2, etc.) from the DWD open data server with timestep filtering.
- HistoricalDownloader: downloads historical climate observation archives.
- MOSMIXDownloader: downloads MOSMIX point-forecast files (MOSMIX_L, MOSMIX_S, MOSMIX-SNOW_S).
- OSUploader: uploads files to S3-compatible or MinIO object storage using parallel workers, and S3-compatible ETag integrity verification (size pre-check + composite hash).
- OSDownloader: downloads files from S3-compatible or MinIO object storage using parallel workers, and S3-compatible ETag integrity verification (size pre-check + composite hash).
- GribFileManager: decompresses BZ2 archives and converts GRIB2 files to CSV.
- DataMerger: filters and merges CSV data frames across variables and levels.
- Notifier: sends status messages about downloads, uploads, and errors to a Gotify server.
- Logging: writes a per-run log file listing the downloaded, failed, and corrupted files.
- Integrity checks: all three HTTP downloaders verify the downloaded file size against the size reported by DWD; object-storage transfers are verified via S3-compatible ETag.
- Parallel processing: downloads and uploads run in parallel with a configurable number of workers. GRIB processing is currently sequential.
Installation
git clone https://github.com/trholy/dwdown.git
cd dwdown
pip install .
Documentation
Read the documentation on GitLab Pages.
Usage
HistoricalDownloader
Fetch historical observations from German weather stations.
from dwdown.download import HistoricalDownloader
# Initialize HistoricalDownloader
scraper = HistoricalDownloader(
base_url=None, # Base URL for historical data (defaults to DWD open data)
files_path=None, # Path for downloaded files (defaults to download_files)
extracted_files_path=None, # Path for extracted files (defaults to extracted_files)
log_files_path="log_files", # Path for log files
encoding=None, # Encoding for station description files (defaults to windows-1252)
station_description_file_name=None, # Station description filename (uses DWD default)
delay=1, # 1 second delay between downloads
retry=0, # Don't retry failed downloads
timeout=30 # 30 second timeout for requests
)
# Download station descriptions
scraper.download_station_description()
# Read station descriptions
station_descriptions = scraper.read_station_description()
print(station_descriptions)
# Get download links for specific stations
links = scraper.get_links(
station_ids=['00001', '00003'], # Zero-padded 5-digit station IDs
prefix="tageswerte_KL", # File prefix for daily weather data
suffix="_hist.zip" # File suffix for historical zip files
)
print(links)
# Download files — post-download size is verified against DWD-reported size
scraper.download(check_for_existence=True)
# Print status after download
print("Successfully downloaded files:", scraper.downloaded_files)
print("Failed downloads:", scraper.failed_files)
print("Download might be corrupted:", scraper.corrupted_files)
# Unpack ZIP files
scraper.extract(unpack_hist_data_only=True, check_for_existence=True)
# Read and save data as CSV
df = scraper.read_data(save_as_csv=True)
print(df)
MOSMIXDownloader
Fetch MOSMIX forecasts, which are derived from global weather models and statistically downscaled to land-based climate stations using their historical observations.
from dwdown.download import MOSMIXDownloader
# Initialize MOSMIXDownloader
# mosmix_type can be "MOSMIX_L" (single- or all-stations files) or "MOSMIX_S" (all-stations files only)
scraper = MOSMIXDownloader(
mosmix_type="MOSMIX_L",
base_url=None, # Base URL constructed automatically based on mosmix_type
files_path=None, # Path for downloaded files (defaults to download_files)
extracted_files_path=None, # Path for extracted files (defaults to extracted_files)
log_files_path="log_files",
delay=1,
retry=0,
timeout=30
)
# Get download links for a specific station.
# For MOSMIX_L, station_ids selects individual station files.
# To pin a specific forecast run, pass a YYYYMMDDhh token via include_pattern —
# e.g. include_pattern=["2026061109"] matches MOSMIX_L_2026061109_01001.kmz
# (single-station) and MOSMIX_L_2026061109.kmz (all-stations) alike.
# Omit include_pattern to download the latest available run.
links = scraper.get_links(
station_ids=['01001'], # Jan Mayen — only relevant for MOSMIX_L
)
print(f"Found {len(links)} links:", links)
# Download files — post-download size is verified against DWD-reported size
scraper.download(check_for_existence=True)
# Print status after download
print("Successfully downloaded files:", scraper.downloaded_files)
print("Failed downloads:", scraper.failed_files)
print("Download might be corrupted:", scraper.corrupted_files)
# Unpack KMZ files to KML
scraper.extract(check_for_existence=True)
# Read KML data into DataFrames
data = scraper.read_data(save_as_csv=True)
if data:
for filename, df in data.items():
print(f"Processed {filename}:")
print(df.head())
else:
print("No data processed.")
ForecastDownloader
Fetch numerical weather predictions (NWP) from DWD.
from dwdown.download import ForecastDownloader
variables = [
'aswdifd_s',
'relhum',
'smi',
]
for variable in variables:
# Initialize ForecastDownloader
dwd_downloader = ForecastDownloader(
url=f"https://opendata.dwd.de/weather/nwp/icon-d2/grib/09/{variable}/",
retry=0,
delay=0.1,
n_jobs=4,
files_path=f"download_files/09/{variable}",
log_files_path="log_files"
)
# Fetch download links
# timesteps: explicit list of integer forecast hours to include; None = no filter
dwd_downloader.get_links(
exclude_pattern=["icosahedral"],
timesteps=list(range(49)), # hours 0–48
)
# Download files
dwd_downloader.download(check_for_existence=True)
# Print status after download
print("Successfully downloaded files:", dwd_downloader.downloaded_files)
print("Failed downloads:", dwd_downloader.failed_files)
print("Download might be corrupted:", dwd_downloader.corrupted_files)
OSUploader
The OSUploader class uploads files to MinIO or any other S3-compatible storage. After each transfer, integrity is verified via S3-compatible ETag (size pre-check + composite hash matching MinIO's multipart ETag algorithm).
from dwdown.upload import OSUploader
# Initialize OSUploader
uploader = OSUploader(
endpoint="your-minio-server.com",
access_key="your-access-key",
secret_key="your-secret-key",
files_path="download_files", # Local directory containing files to upload
bucket_name="weather-forecasts", # Target bucket
secure=False, # True for HTTPS, False for HTTP
log_files_path="log_files",
n_jobs=4 # Parallel upload workers
)
# Upload files — integrity is verified via S3-compatible ETag (size pre-check + composite hash)
uploader.upload()
# Delete local files that were successfully uploaded
uploader.delete()
# Print status after upload
print("Successfully uploaded files:", uploader.uploaded_files)
print("Upload might be corrupted:", uploader.corrupted_files)
OSDownloader
The OSDownloader class downloads files from MinIO or any other S3-compatible storage. Integrity is verified via S3-compatible ETag after each transfer. Note that unlike the HTTP downloaders, there is no separate failed_files list — all failures (network errors and integrity mismatches) are collected in corrupted_files.
from dwdown.download import OSDownloader
# Initialize OSDownloader
minio_downloader = OSDownloader(
endpoint="your-minio-server.com",
access_key="your-access-key",
secret_key="your-secret-key",
files_path="download_files", # Local directory to save downloaded files
bucket_name="weather-forecasts", # Source bucket
secure=False, # True for HTTPS, False for HTTP
log_files_path="log_files",
n_jobs=4 # Parallel download workers
)
# Download files — integrity is verified via S3-compatible ETag (size pre-check + composite hash)
minio_downloader.download(check_for_existence=True)
# Print status after download
# Note: OSDownloader has no separate failed_files list — all failures
# (network errors and integrity mismatches) are collected in corrupted_files.
print("Successfully downloaded files:", minio_downloader.downloaded_files)
print("Download might be corrupted:", minio_downloader.corrupted_files)
# Optionally delete the local copies of the downloaded files
# minio_downloader.delete()
GribFileManager and DataMerger
GribFileManager takes care of decompressing and converting GRIB2 files; DataMerger then merges and filters the resulting CSV data frames across variables and levels.
from dwdown.processing import DataMerger, GribFileManager
# Initialize GribFileManager
processor = GribFileManager(
files_path="download_files", # Directory containing .bz2/.grib2 files
extracted_files_path="extracted_files",
converted_files_path="csv_files",
)
# Retrieve downloaded filenames
file_names = processor.get_filenames()
# Decompress BZ2, convert GRIB2 to CSV, and optionally apply a geographic bounding box
processor.get_csv(
file_names=file_names,
apply_geo_filtering=True,
start_lat=50.840,
end_lat=51.000,
start_lon=11.470,
end_lon=11.690,
)
# Variables to build merged dataframe from
variables = [
'aswdifd_s',
'relhum',
'smi',
]
# Mapping from DWD variable names to GRIB (ecCodes) short names
mapping_dictionary = {
'aswdifd_s': 'ASWDIFD_S',
'relhum': 'r',
'smi': 'SMI',
}
# Per-variable level filter: only keep files whose level is in the given set
additional_patterns = {
"relhum": [200, 975, 1000],
"smi": [0, 9, 27],
}
# Initialize DataMerger
data_editor = DataMerger(
files_path='csv_files/09/',
required_columns={'latitude', 'longitude', 'valid_time'},
join_method='inner',
mapping_dictionary=mapping_dictionary,
additional_patterns=additional_patterns,
)
df = data_editor.merge(
time_step=0,
variables=variables
)
print("Processed DataFrame:", df)
df.to_csv('processed_dataframe.csv')
Notifier
The Notifier class sends status updates via a Gotify server.
from minio import Minio
from dwdown.notify import Notifier
notifier = Notifier(
server_url="your-gotify-server.com",
token="your-access-token",
priority=5,
secure=True
)
minio_client = Minio(
endpoint="your-minio-server.com",
access_key="your-access-key",
secret_key="your-secret-key",
secure=False
)
buckets = minio_client.list_buckets()
status_dict = {}
for bucket in buckets:
bucket_name = bucket.name
print(f"Processing bucket: {bucket_name}")
objects = minio_client.list_objects(bucket_name, recursive=True)
status_dict[bucket_name] = [len([obj.object_name for obj in objects])]
notifier.send_notification(
message=status_dict,
script_name="downloader"
)
Directory structure
./
├── .git
├── .gitignore
├── .gitlab-ci.yml
├── LICENSE
├── README.md
├── THIRD_PARTY_LICENSES.txt
├── docs
│ ├── data
│ │ └── MappingStore.md
│ ├── download
│ │ ├── ForecastDownloader.md
│ │ ├── HistoricalDownloader.md
│ │ ├── MosmixDownloader.md
│ │ └── OSDownloader.md
│ ├── notify
│ │ └── Notifier.md
│ ├── processing
│ │ ├── DataMerger.md
│ │ └── GribFileManager.md
│ ├── upload
│ │ └── OSUploader.md
│ └── utils
│ ├── DataFrameOperator.md
│ ├── DateTimeUtils.md
│ ├── FileHandler.md
│ ├── LogHandler.md
│ ├── NetworkHandlers.md
│ ├── OSHandler.md
│ └── Utilities.md
├── example_usage
│ ├── 00_dwd_forecast_scraper.py
│ ├── 00b_dwd_hist-station-data_scraper.py
│ ├── 00c_dwd_mosmix_scraper.py
│ ├── 01_os_uploader.py
│ ├── 02_os_downloader.py
│ └── 03_data_processing.py
├── img
│ └── example_workflow.png
├── mkdocs.yml
├── pyproject.toml
├── setup.py
├── src
│ └── dwdown
│ ├── __init__.py
│ ├── data
│ │ ├── __init__.py
│ │ └── mapping.py
│ ├── download
│ │ ├── __init__.py
│ │ ├── forecast_download.py
│ │ ├── historical_download.py
│ │ ├── mosmix_download.py
│ │ └── os_download.py
│ ├── notify
│ │ ├── __init__.py
│ │ └── notifier.py
│ ├── processing
│ │ ├── __init__.py
│ │ ├── data_merging.py
│ │ └── grib_data_handling.py
│ ├── upload
│ │ ├── __init__.py
│ │ └── os_upload.py
│ └── utils
│ ├── __init__.py
│ ├── date_time_utilis.py
│ ├── df_utilis.py
│ ├── file_handling.py
│ ├── general_utilis.py
│ ├── log_handling.py
│ ├── network_handling.py
│ └── os_handling.py
└── tests
├── integration
│ ├── __init__.py
│ ├── test_historical_workflow.py
│ ├── test_mosmix_workflow.py
│ ├── test_nwp_forecast_workflow.py
│ └── test_os_workflow.py
├── test_ForecastDownloader.py
├── test_HistoricalDownloader.py
├── test_MOSMIXDownloader.py
├── test_OSDownloader.py
├── test_OSUploader.py
├── test_date_time_utilis.py
├── test_file_handling.py
├── test_log_handling.py
├── test_mapping.py
├── test_network_handling.py
├── test_notifier.py
├── test_os_handling.py
├── test_processing.py
└── test_utils.py
License
This project is licensed under the MIT License. See the LICENSE file for more details.
Contributing
Contributions are welcome; please open an issue or a pull request.