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.
- docker/Dockerfile +30 -0
- docker/docker-compose.yaml +58 -0
- docker/docker-compose_test.yaml +24 -0
- docker/start-docker-test.sh +6 -0
- docs/requirements.txt +10 -0
- docs/source/Changelog.md +2 -0
- docs/source/License.rst +7 -0
- docs/source/Methode.md +161 -0
- docs/source/_static/custom.css +8 -0
- docs/source/_static/favicon.ico +0 -0
- docs/source/_static/logo.png +0 -0
- docs/source/api/api.rst +15 -0
- docs/source/api/cli.rst +8 -0
- docs/source/api/weatherDB.broker.rst +10 -0
- docs/source/api/weatherDB.config.rst +7 -0
- docs/source/api/weatherDB.db.rst +23 -0
- docs/source/api/weatherDB.rst +22 -0
- docs/source/api/weatherDB.station.rst +56 -0
- docs/source/api/weatherDB.stations.rst +46 -0
- docs/source/api/weatherDB.utils.rst +22 -0
- docs/source/conf.py +137 -0
- docs/source/index.rst +33 -0
- docs/source/setup/Configuration.md +127 -0
- docs/source/setup/Hosting.md +9 -0
- docs/source/setup/Install.md +49 -0
- docs/source/setup/Quickstart.md +183 -0
- docs/source/setup/setup.rst +12 -0
- weatherdb/__init__.py +24 -0
- weatherdb/_version.py +1 -0
- weatherdb/alembic/README.md +8 -0
- weatherdb/alembic/alembic.ini +80 -0
- weatherdb/alembic/config.py +9 -0
- weatherdb/alembic/env.py +100 -0
- weatherdb/alembic/script.py.mako +26 -0
- weatherdb/alembic/versions/V1.0.0_initial_database_creation.py +898 -0
- weatherdb/alembic/versions/V1.0.2_more_charachters_for_settings+term_station_ma_raster.py +88 -0
- weatherdb/alembic/versions/V1.0.5_fix-ma-raster-values.py +152 -0
- weatherdb/alembic/versions/V1.0.6_update-views.py +22 -0
- weatherdb/broker.py +667 -0
- weatherdb/cli.py +214 -0
- weatherdb/config/ConfigParser.py +663 -0
- weatherdb/config/__init__.py +5 -0
- weatherdb/config/config_default.ini +162 -0
- weatherdb/db/__init__.py +3 -0
- weatherdb/db/connections.py +374 -0
- weatherdb/db/fixtures/RichterParameters.json +34 -0
- weatherdb/db/models.py +402 -0
- weatherdb/db/queries/get_quotient.py +155 -0
- weatherdb/db/views.py +165 -0
- weatherdb/station/GroupStation.py +710 -0
- weatherdb/station/StationBases.py +3108 -0
- weatherdb/station/StationET.py +111 -0
- weatherdb/station/StationP.py +807 -0
- weatherdb/station/StationPD.py +98 -0
- weatherdb/station/StationT.py +164 -0
- weatherdb/station/__init__.py +13 -0
- weatherdb/station/constants.py +21 -0
- weatherdb/stations/GroupStations.py +519 -0
- weatherdb/stations/StationsBase.py +1021 -0
- weatherdb/stations/StationsBaseTET.py +30 -0
- weatherdb/stations/StationsET.py +17 -0
- weatherdb/stations/StationsP.py +128 -0
- weatherdb/stations/StationsPD.py +24 -0
- weatherdb/stations/StationsT.py +21 -0
- weatherdb/stations/__init__.py +11 -0
- weatherdb/utils/TimestampPeriod.py +369 -0
- weatherdb/utils/__init__.py +3 -0
- weatherdb/utils/dwd.py +350 -0
- weatherdb/utils/geometry.py +69 -0
- weatherdb/utils/get_data.py +285 -0
- weatherdb/utils/logging.py +126 -0
- weatherdb-1.1.0.dist-info/LICENSE +674 -0
- weatherdb-1.1.0.dist-info/METADATA +765 -0
- weatherdb-1.1.0.dist-info/RECORD +77 -0
- weatherdb-1.1.0.dist-info/WHEEL +5 -0
- weatherdb-1.1.0.dist-info/entry_points.txt +2 -0
- 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)
|