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.
- 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)
|