weatherdb 1.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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)