weatherdb 1.1.0__py3-none-any.whl

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 (77) hide show
  1. docker/Dockerfile +30 -0
  2. docker/docker-compose.yaml +58 -0
  3. docker/docker-compose_test.yaml +24 -0
  4. docker/start-docker-test.sh +6 -0
  5. docs/requirements.txt +10 -0
  6. docs/source/Changelog.md +2 -0
  7. docs/source/License.rst +7 -0
  8. docs/source/Methode.md +161 -0
  9. docs/source/_static/custom.css +8 -0
  10. docs/source/_static/favicon.ico +0 -0
  11. docs/source/_static/logo.png +0 -0
  12. docs/source/api/api.rst +15 -0
  13. docs/source/api/cli.rst +8 -0
  14. docs/source/api/weatherDB.broker.rst +10 -0
  15. docs/source/api/weatherDB.config.rst +7 -0
  16. docs/source/api/weatherDB.db.rst +23 -0
  17. docs/source/api/weatherDB.rst +22 -0
  18. docs/source/api/weatherDB.station.rst +56 -0
  19. docs/source/api/weatherDB.stations.rst +46 -0
  20. docs/source/api/weatherDB.utils.rst +22 -0
  21. docs/source/conf.py +137 -0
  22. docs/source/index.rst +33 -0
  23. docs/source/setup/Configuration.md +127 -0
  24. docs/source/setup/Hosting.md +9 -0
  25. docs/source/setup/Install.md +49 -0
  26. docs/source/setup/Quickstart.md +183 -0
  27. docs/source/setup/setup.rst +12 -0
  28. weatherdb/__init__.py +24 -0
  29. weatherdb/_version.py +1 -0
  30. weatherdb/alembic/README.md +8 -0
  31. weatherdb/alembic/alembic.ini +80 -0
  32. weatherdb/alembic/config.py +9 -0
  33. weatherdb/alembic/env.py +100 -0
  34. weatherdb/alembic/script.py.mako +26 -0
  35. weatherdb/alembic/versions/V1.0.0_initial_database_creation.py +898 -0
  36. weatherdb/alembic/versions/V1.0.2_more_charachters_for_settings+term_station_ma_raster.py +88 -0
  37. weatherdb/alembic/versions/V1.0.5_fix-ma-raster-values.py +152 -0
  38. weatherdb/alembic/versions/V1.0.6_update-views.py +22 -0
  39. weatherdb/broker.py +667 -0
  40. weatherdb/cli.py +214 -0
  41. weatherdb/config/ConfigParser.py +663 -0
  42. weatherdb/config/__init__.py +5 -0
  43. weatherdb/config/config_default.ini +162 -0
  44. weatherdb/db/__init__.py +3 -0
  45. weatherdb/db/connections.py +374 -0
  46. weatherdb/db/fixtures/RichterParameters.json +34 -0
  47. weatherdb/db/models.py +402 -0
  48. weatherdb/db/queries/get_quotient.py +155 -0
  49. weatherdb/db/views.py +165 -0
  50. weatherdb/station/GroupStation.py +710 -0
  51. weatherdb/station/StationBases.py +3108 -0
  52. weatherdb/station/StationET.py +111 -0
  53. weatherdb/station/StationP.py +807 -0
  54. weatherdb/station/StationPD.py +98 -0
  55. weatherdb/station/StationT.py +164 -0
  56. weatherdb/station/__init__.py +13 -0
  57. weatherdb/station/constants.py +21 -0
  58. weatherdb/stations/GroupStations.py +519 -0
  59. weatherdb/stations/StationsBase.py +1021 -0
  60. weatherdb/stations/StationsBaseTET.py +30 -0
  61. weatherdb/stations/StationsET.py +17 -0
  62. weatherdb/stations/StationsP.py +128 -0
  63. weatherdb/stations/StationsPD.py +24 -0
  64. weatherdb/stations/StationsT.py +21 -0
  65. weatherdb/stations/__init__.py +11 -0
  66. weatherdb/utils/TimestampPeriod.py +369 -0
  67. weatherdb/utils/__init__.py +3 -0
  68. weatherdb/utils/dwd.py +350 -0
  69. weatherdb/utils/geometry.py +69 -0
  70. weatherdb/utils/get_data.py +285 -0
  71. weatherdb/utils/logging.py +126 -0
  72. weatherdb-1.1.0.dist-info/LICENSE +674 -0
  73. weatherdb-1.1.0.dist-info/METADATA +765 -0
  74. weatherdb-1.1.0.dist-info/RECORD +77 -0
  75. weatherdb-1.1.0.dist-info/WHEEL +5 -0
  76. weatherdb-1.1.0.dist-info/entry_points.txt +2 -0
  77. weatherdb-1.1.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,285 @@
1
+ """
2
+ Some utilities functions to download the needed data for the module to work.
3
+ """
4
+ import requests
5
+ from pathlib import Path
6
+ from distutils.util import strtobool
7
+ import hashlib
8
+ import progressbar as pb
9
+
10
+ from ..config import config
11
+
12
+ def download_ma_rasters(which="all", overwrite=None, update_user_config=False):
13
+ """Get the multi annual rasters on which bases the regionalisation is done.
14
+
15
+ The refined multi annual datasets, that are downloaded are published on Zenodo [1]_
16
+
17
+ References
18
+ ----------
19
+ .. [1] Schmit, M.; Weiler, M. (2023). German weather services (DWD) multi annual meteorological rasters for the climate period 1991-2020 refined to 25m grid (1.0.0) [Data set]. Zenodo. https://doi.org/10.5281/zenodo.10066045
20
+
21
+ Parameters
22
+ ----------
23
+ which : str or [str], optional
24
+ Which raster to download.
25
+ Options are "dwd", "hyras", "regnie" and "all".
26
+ The default is "all".
27
+ overwrite : bool, optional
28
+ Should the multi annual rasters be downloaded even if they already exist?
29
+ If None the user will be asked.
30
+ The default is None.
31
+ update_user_config : bool, optional
32
+ Should the downloaded rasters be set as the regionalisation rasters in the user configuration file?
33
+ The default is False.
34
+ """
35
+ # DOI of the multi annual dataset
36
+ DOI = "10.5281/zenodo.10066045"
37
+
38
+ # check which
39
+ if isinstance(which, str):
40
+ which = [which]
41
+ for w in which:
42
+ if w not in ["all", "dwd", "hyras", "regnie"]:
43
+ raise ValueError(
44
+ "which must be one of 'all', 'dwd', 'hyras' or 'regnie'.")
45
+ if w == "all":
46
+ which = ["dwd", "hyras", "regnie"]
47
+ break
48
+
49
+ # get zenodo record
50
+ zenodo_id = requests.get(
51
+ f"https://doi.org/{DOI}"
52
+ ).url.split("/")[-1]
53
+ zenodo_rec = requests.get(
54
+ f"https://zenodo.org/api/records/{zenodo_id}"
55
+ ).json()
56
+
57
+ # download files
58
+ for file in zenodo_rec["files"]:
59
+ file_key = file["key"].lower().split("_")[0].split("-")[0]
60
+ if file_key in which:
61
+ # check if file is in config
62
+ if f"data:rasters:{file_key}" not in config:
63
+ print(f"Skipping {file_key} as it is not in your configuration.\nPlease add a section 'data:rasters:{file_key}' to your configuration file.")
64
+ continue
65
+
66
+ # check if file already exists
67
+ file_path = Path(config.get(f"data:rasters:{file_key}", "file"))
68
+ if file_path.exists():
69
+ skip = False
70
+ if overwrite is False:
71
+ skip = True
72
+ elif overwrite is None:
73
+ skip = not strtobool(input(
74
+ f"{file_key} already exists at {file_path}.\n"+
75
+ "Do you want to overwrite it? [y/n] "))
76
+
77
+ if skip:
78
+ print(f"Skipping {file_key} as overwriting is not allowed.")
79
+ continue
80
+
81
+ # check if the directory exists
82
+ if not file_path.parent.exists():
83
+ if strtobool(input(
84
+ f"The directory \"{file_path.parent}\" does not exist.\n"+
85
+ "Do you want to create it? [y/n] ")):
86
+ file_path.parent.mkdir(parents=True)
87
+
88
+ # download file
89
+ r = requests.get(file["links"]["self"], stream=True)
90
+ if r.status_code != 200:
91
+ r.raise_for_status() # Will only raise for 4xx codes, so...
92
+ raise RuntimeError(
93
+ f'Request to {file["links"]["self"]} returned status code {r.status_code}')
94
+ block_size = 1024
95
+ file_size = int(r.headers.get('Content-Length', 0))
96
+ pbar = pb.ProgressBar(
97
+ max_value=file_size,
98
+ prefix=f"downloading {file_key}: ",
99
+ widgets=[ " ",
100
+ pb.widgets.DataSize(),
101
+ "/",
102
+ pb.widgets.DataSize("max_value"),
103
+ pb.widgets.AdaptiveTransferSpeed(
104
+ format='(%(scaled)5.1f %(prefix)s%(unit)-s/s) '),
105
+ pb.widgets.Bar(), " ",
106
+ pb.widgets.Percentage(),
107
+ pb.widgets.ETA()],
108
+ line_breaks=False,
109
+ redirect_stdout=True
110
+ ).start()
111
+ md5 = hashlib.md5()
112
+ with open(file_path, "wb+") as f:
113
+ for i, chunk in enumerate(r.iter_content(block_size)):
114
+ f.write(chunk)
115
+ md5.update(chunk)
116
+ pbar.update(i*block_size)
117
+ pbar.finish()
118
+
119
+ # check checksum
120
+ if md5.hexdigest() != file["checksum"].replace("md5:", ""):
121
+ raise ValueError(
122
+ f"Checksum of {file_key} doesn't match. File might be corrupted.")
123
+
124
+ # update user config
125
+ if update_user_config:
126
+ if config.has_user_config:
127
+ config.update_user_config(f"data:rasters:{file_key}", "file", str(file_path))
128
+ else:
129
+ print(f"No user configuration file found, therefor the raster '{file_key}' is not set in the user configuration file.")
130
+
131
+
132
+ def download_dem(overwrite=None, extent=(5.3, 46.1, 15.6, 55.4), update_user_config=False):
133
+ """Download the newest DEM data from the Copernicus Sentinel dataset.
134
+
135
+ Only the GLO-30 DEM, which has a 30m resolution, is downloaded as it is freely available.
136
+ If you register as a scientific researcher also the EEA-10, with 10 m resolution, is available.
137
+ You will have to download the data yourself and define it in the configuration file.
138
+
139
+ After downloading the data, the files are merged and saved as a single tif file in the data directory in a subfolder called 'DEM'.
140
+ To use the DEM data in the WeatherDB, you will have to define the path to the tif file in the configuration file.
141
+
142
+ Source:
143
+ Copernicus DEM - Global and European Digital Elevation Model. Digital Surface Model (DSM) provided in 3 different resolutions (90m, 30m, 10m) with varying geographical extent (EEA: European and GLO: global) and varying format (INSPIRE, DGED, DTED). DOI:10.5270/ESA-c5d3d65.
144
+
145
+ Parameters
146
+ ----------
147
+ overwrite : bool, optional
148
+ Should the DEM data be downloaded even if it already exists?
149
+ If None the user will be asked.
150
+ The default is None.
151
+ extent : tuple, optional
152
+ The extent in WGS84 of the DEM data to download.
153
+ The default is the boundary of germany + ~40km = (5.3, 46.1, 15.6, 55.4).
154
+ update_user_config : bool, optional
155
+ Should the downloaded DEM be set as the used DEM in the user configuration file?
156
+ The default is False.
157
+ """
158
+ # import necessary modules
159
+ import rasterio as rio
160
+ from rasterio.merge import merge
161
+ import tarfile
162
+ import shutil
163
+ from tempfile import TemporaryDirectory
164
+ import re
165
+ import json
166
+
167
+ # get dem_dir
168
+ base_dir = Path(config.get("data", "base_dir"))
169
+ dem_dir = base_dir / "DEM"
170
+ dem_dir.mkdir(parents=True, exist_ok=True)
171
+
172
+ # get available datasets
173
+ prism_url = "https://prism-dem-open.copernicus.eu/pd-desk-open-access/publicDemURLs"
174
+ avl_ds_req = json.loads(
175
+ requests.get(
176
+ prism_url,
177
+ headers={"Accept": "json"}
178
+ ).text
179
+ )
180
+ avl_ds = [{
181
+ "id": e["datasetId"],
182
+ "year": int(e["datasetId"].split("/")[1].split("_")[0]),
183
+ "year_part": int(e["datasetId"].split("/")[1].split("_")[1]),
184
+ "resolution": int(e["datasetId"].split("-")[2]),
185
+ } for e in avl_ds_req]
186
+
187
+ # select newest and highest resolution dataset
188
+ ds_id = sorted(
189
+ avl_ds,
190
+ key=lambda x: (-x["resolution"], x["year"], x["year_part"])
191
+ )[-1]["id"]
192
+
193
+ # check if dataset already exists
194
+ dem_file = dem_dir / f'{ds_id.replace("/", "__")}.tif'
195
+ if dem_file.exists():
196
+ print(f"The DEM data already exists at {dem_file}.")
197
+ if overwrite is None:
198
+ overwrite = strtobool(input("Do you want to overwrite it? [y/n] "))
199
+ if not overwrite:
200
+ print("Skipping, because overwritting was turned of.")
201
+ return
202
+ else:
203
+ print("Overwriting the dataset.")
204
+ dem_dir.mkdir(exist_ok=True)
205
+
206
+ # selecting DEM tiles
207
+ print(f"getting available tiles for Copernicus dataset '{ds_id}'")
208
+ ds_files_req = json.loads(
209
+ requests.get(
210
+ f"{prism_url}/{ds_id.replace('/', '__')}",
211
+ headers={"Accept": "json"}
212
+ ).text
213
+ )
214
+ re_comp = re.compile(r".*/Copernicus_DSM_\d{2}_N\d*_\d{2}_E\d*.*")
215
+ ds_files_all = [
216
+ {"lat": int(Path(f["nativeDemUrl"]).stem.split("_")[3][1:]),
217
+ "long": int(Path(f["nativeDemUrl"]).stem.split("_")[5][1:]),
218
+ **f} for f in ds_files_req if re_comp.match(f["nativeDemUrl"])]
219
+ res_deg = 1
220
+ ds_files = list(filter(
221
+ lambda x: (
222
+ (extent[0] - res_deg) < x["long"] < extent[2] and
223
+ (extent[1] - res_deg) < x["lat"] < extent[3]
224
+ ),
225
+ ds_files_all))
226
+
227
+ # download DEM tiles
228
+ print("downloading tiles")
229
+ with TemporaryDirectory() as tmp_dir:
230
+ tmp_dir_fp = Path(tmp_dir)
231
+ for f in pb.progressbar(ds_files):
232
+ with open(tmp_dir_fp / Path(f["nativeDemUrl"]).name, "wb") as d:
233
+ d.write(requests.get(f["nativeDemUrl"]).content)
234
+ print("downloaded all files")
235
+
236
+ # extracting tifs from tars
237
+ for i, f in pb.progressbar(list(enumerate(tmp_dir_fp.glob("*.tar")))):
238
+ with tarfile.open(f) as t:
239
+ # extract dem tif
240
+ re_comp = re.compile(r"^.*\/DEM\/.*\.tif$")
241
+ name = list(filter(re_comp.match, t.getnames()))[0]
242
+ with open(tmp_dir_fp/f"{name.split('/')[-1]}", "wb") as d:
243
+ d.write(t.extractfile(name).read())
244
+
245
+ # extract info contract
246
+ if i==0:
247
+ re_comp = re.compile(r"^.*\/INFO\/.*\.pdf$")
248
+ name = list(filter(re_comp.match, t.getnames()))[0]
249
+ with open(tmp_dir_fp/f"{name.split('/')[-1]}", "wb") as d:
250
+ d.write(t.extractfile(name).read())
251
+
252
+ # remove tar
253
+ f.unlink()
254
+
255
+ # merge files
256
+ srcs = [rio.open(f) for f in tmp_dir_fp.glob("*.tif")]
257
+ dem_np, dem_tr = merge(srcs)
258
+ dem_meta = srcs[0].meta.copy()
259
+ dem_meta.update({
260
+ "driver": "GTiff",
261
+ "height": dem_np.shape[1],
262
+ "width": dem_np.shape[2],
263
+ "transform": dem_tr
264
+ })
265
+ with rio.open(dem_file, "w", **dem_meta) as d:
266
+ d.write(dem_np)
267
+
268
+ # copy info contract
269
+ tmp_eula_fp = next(tmp_dir_fp.glob("*.pdf"))
270
+ shutil.copyfile(
271
+ tmp_eula_fp,
272
+ dem_dir / tmp_eula_fp.name
273
+ )
274
+
275
+ print(f"created DEM at '{dem_file}'.")
276
+
277
+ # update user config
278
+ if update_user_config:
279
+ if config.has_user_config:
280
+ config.update_user_config("data:rasters", "dems", str(dem_file))
281
+ return
282
+ else:
283
+ print("No user configuration file found, therefor the DEM is not set in the user configuration file.")
284
+
285
+ print("To use the DEM data in the WeatherDB, you will have to define the path to the tif file in the user configuration file.")
@@ -0,0 +1,126 @@
1
+ import logging
2
+ from logging.handlers import TimedRotatingFileHandler
3
+ import datetime
4
+ from pathlib import Path
5
+ import re
6
+ import socket
7
+ import os
8
+ import gzip
9
+ import shutil
10
+
11
+ from ..config import config
12
+
13
+ try:
14
+ import coloredlogs
15
+ cl_available = True
16
+ except ImportError:
17
+ cl_available = False
18
+
19
+ # set the log
20
+ #############
21
+ log = logging.getLogger(__name__.split(".")[0])
22
+
23
+ def _get_log_dir ():
24
+ return Path(config.get("logging", "directory"))
25
+
26
+ def remove_old_logs(max_days=14):
27
+ # remove old logs
28
+ log_dir = _get_log_dir()
29
+ log_date_min = datetime.datetime.now() - datetime.timedelta(days=max_days)
30
+ for log_file in [
31
+ file for file in log_dir.glob("*.log.*")
32
+ if re.match(r".*\.log\.\d{4}-\d{2}-\d{2}$", file.name)]:
33
+ try:
34
+ file_date = datetime.datetime.strptime(log_file.name.split(".")[-1], "%Y-%m-%d")
35
+ if file_date < log_date_min:
36
+ log_file.unlink()
37
+ except:
38
+ pass
39
+
40
+ def setup_logging_handlers():
41
+ """Setup the logging handlers depending on the configuration.
42
+
43
+ Raises
44
+ ------
45
+ ValueError
46
+ If the handler type is not known.
47
+ """
48
+ # get log dir
49
+ log_dir = _get_log_dir()
50
+ if not log_dir.is_dir(): log_dir.mkdir()
51
+
52
+ # add filehandler if necessary
53
+ log.setLevel(config.get("logging", "level", fallback=logging.DEBUG))
54
+ handler_names = [h.get_name() for h in log.handlers]
55
+ format = config.get(
56
+ "logging",
57
+ "format",
58
+ raw=True,
59
+ fallback="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
60
+ level = config.get("logging", "level", fallback=logging.DEBUG)
61
+ for handler_type in config.get_list("logging", "handlers"):
62
+ handler_name = f"weatherDB_config:{handler_type}"
63
+
64
+ # check if coloredlogs is available
65
+ if cl_available and handler_type == "console":
66
+ coloredlogs.install(level=level, fmt=format, logger=log)
67
+ log.debug("Using coloredlogs")
68
+ continue
69
+
70
+ # get log file name
71
+ if handler_type == "file":
72
+ try:
73
+ user = os.getlogin()
74
+ except:
75
+ user = "anonym"
76
+ host = socket.gethostname().replace(".","_")
77
+ log_file = log_dir.joinpath(
78
+ config.get("logging", "file", fallback="weatherDB_{user}_{host}.log")\
79
+ .format(user=user, host=host))
80
+
81
+ # get or create handler
82
+ if handler_name not in handler_names:
83
+ if handler_type == "console":
84
+ handler = logging.StreamHandler()
85
+ elif handler_type == "file":
86
+ handler = TimedRotatingFileHandler(
87
+ log_file,
88
+ when="midnight",
89
+ encoding="utf-8")
90
+ if config.getboolean("logging", "compression", fallback=True):
91
+ def namer(name):
92
+ return name + ".gz"
93
+ def rotator(source, dest):
94
+ with open(source, 'rb') as f_in:
95
+ with gzip.open(dest, 'wb') as f_out:
96
+ shutil.copyfileobj(f_in, f_out)
97
+ os.remove(source)
98
+ handler.namer = namer
99
+ handler.rotator = rotator
100
+ else:
101
+ raise ValueError(f"Handler '{handler_type}' not known.")
102
+
103
+ handler.set_name(handler_name)
104
+ log.addHandler(handler)
105
+
106
+ elif handler_name in handler_names:
107
+ handler = log.handlers[handler_names.index(handler_name)]
108
+
109
+ # check if file path has changed
110
+ if handler_type == "file" and handler.baseFilename != str(log_file):
111
+ log.removeHandler(handler)
112
+ handler.close()
113
+ handler = TimedRotatingFileHandler(
114
+ log_file,
115
+ when="midnight",
116
+ encoding="utf-8")
117
+ handler.set_name(handler_name)
118
+ log.addHandler(handler)
119
+
120
+ # set formatter and level
121
+ handler.setFormatter(
122
+ logging.Formatter(format))
123
+ handler.setLevel(level)
124
+
125
+ # add config listener
126
+ config.add_listener("logging", None, setup_logging_handlers)