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,663 @@
|
|
1
|
+
"""
|
2
|
+
The configuration module for the WeatherDB module.
|
3
|
+
"""
|
4
|
+
import configparser
|
5
|
+
from pathlib import Path
|
6
|
+
import keyring
|
7
|
+
from getpass import getpass
|
8
|
+
import sqlalchemy as sa
|
9
|
+
import textwrap
|
10
|
+
import re
|
11
|
+
import os
|
12
|
+
from datetime import datetime, timezone
|
13
|
+
|
14
|
+
# create the config parser class
|
15
|
+
class ConfigParser(configparser.ConfigParser):
|
16
|
+
"""This is the class/object of the configurations for the WeatherDB module.
|
17
|
+
"""
|
18
|
+
_DEFAULT_CONFIG_FILE = Path(__file__).parent.resolve()/'config_default.ini'
|
19
|
+
_MAIN_CONFIG_FILE = Path(__file__).parent.resolve()/'config_main.ini'
|
20
|
+
|
21
|
+
def __init__(self, *args, **kwargs):
|
22
|
+
"""
|
23
|
+
Warning
|
24
|
+
-------
|
25
|
+
You shouldn't initialize this class directly, but use the :py:data:``weatherdb.config`` object from the WeatherDB module, which is an instance of this class.
|
26
|
+
"""
|
27
|
+
super().__init__(
|
28
|
+
interpolation=configparser.ExtendedInterpolation(),
|
29
|
+
*args,
|
30
|
+
**kwargs)
|
31
|
+
|
32
|
+
self._system_listeners = [
|
33
|
+
# (section_name, option_name, callback_function)
|
34
|
+
("main", "user_config_file", self._write_main_config)
|
35
|
+
]
|
36
|
+
self._user_listeners = []
|
37
|
+
|
38
|
+
# read the configuration files
|
39
|
+
self.read(self._DEFAULT_CONFIG_FILE)
|
40
|
+
self._read_main_config()
|
41
|
+
self.load_user_config(raise_undefined_error=False)
|
42
|
+
self.load_environment_variables()
|
43
|
+
|
44
|
+
def add_listener(self, section, option, callback):
|
45
|
+
"""Add a callback function to be called when a configuration option is changed.
|
46
|
+
|
47
|
+
Parameters
|
48
|
+
----------
|
49
|
+
section : str
|
50
|
+
The section of the configuration file.
|
51
|
+
If None, the callback will be called for every change.
|
52
|
+
option : str
|
53
|
+
The option of the configuration file.
|
54
|
+
If None, the callback will be called for every change in the given section.
|
55
|
+
callback : function
|
56
|
+
The function to be called when the configuration option is changed.
|
57
|
+
"""
|
58
|
+
if (section, option, callback) not in self._user_listeners:
|
59
|
+
self._user_listeners.append((section, option, callback))
|
60
|
+
|
61
|
+
def remove_listener(self, section, option, callback="_all_"):
|
62
|
+
"""Remove a callback function from the list of callbacks.
|
63
|
+
|
64
|
+
Parameters
|
65
|
+
----------
|
66
|
+
section : str or None
|
67
|
+
The section of the configuration file.
|
68
|
+
If "_all_", the callback will be removed for every change.
|
69
|
+
option : str or None
|
70
|
+
The option of the configuration file.
|
71
|
+
If "_all_", the callback will be removed for every change in the given section.
|
72
|
+
callback : function or str, optional
|
73
|
+
The function to be removed from the list of callbacks.
|
74
|
+
If "_all_", all callbacks for the given section and option will be removed.
|
75
|
+
The default is "_all_".
|
76
|
+
"""
|
77
|
+
drop_cbs = []
|
78
|
+
for cb in self._user_listeners:
|
79
|
+
if (section == "_all_") or (cb[0] == section):
|
80
|
+
if (option == "_all_") or (cb[1] == option):
|
81
|
+
if (callback is None) or (cb[2] == callback):
|
82
|
+
drop_cbs.append(cb)
|
83
|
+
for cb in drop_cbs:
|
84
|
+
self._user_listeners.remove(cb)
|
85
|
+
|
86
|
+
def _read_main_config(self):
|
87
|
+
self.read(self._MAIN_CONFIG_FILE)
|
88
|
+
self._set(
|
89
|
+
"main",
|
90
|
+
"module_path",
|
91
|
+
Path(__file__).resolve().parent.parent.as_posix())
|
92
|
+
|
93
|
+
def _write_main_config(self):
|
94
|
+
with open(self._MAIN_CONFIG_FILE, "w+") as fp:
|
95
|
+
self._write_section(
|
96
|
+
fp=fp,
|
97
|
+
section_name="main",
|
98
|
+
section_items=self._sections["main"].items(),
|
99
|
+
delimiter=self._delimiters[0])
|
100
|
+
|
101
|
+
def _set(self, section, option, value):
|
102
|
+
"""The internal function to set a configuration option for the WeatherDB module.
|
103
|
+
|
104
|
+
Please use set instead.
|
105
|
+
|
106
|
+
Parameters
|
107
|
+
----------
|
108
|
+
section : str
|
109
|
+
A section of the configuration file.
|
110
|
+
See config_default.ini for available sections.
|
111
|
+
option : str
|
112
|
+
The option to be changed.
|
113
|
+
See config_default.ini for available options and explanations.
|
114
|
+
value : str, int, bool or list
|
115
|
+
The new value for the option.
|
116
|
+
"""
|
117
|
+
if section not in self.sections():
|
118
|
+
self.add_section(section)
|
119
|
+
if isinstance(value, list):
|
120
|
+
value = ",\n\t".join([str(val) for val in value])
|
121
|
+
if option not in self[section] or (value.replace("\t", "") != self.get(section, option)):
|
122
|
+
super().set(section, option, value)
|
123
|
+
|
124
|
+
# fire the change listeners
|
125
|
+
for cb_section, cb_option, cb in self._system_listeners + self._user_listeners:
|
126
|
+
if (cb_section is None or cb_section == section):
|
127
|
+
if (cb_option is None or cb_option == option):
|
128
|
+
cb()
|
129
|
+
|
130
|
+
def set(self, section, option, value):
|
131
|
+
"""Set a configuration option for the WeatherDB module.
|
132
|
+
|
133
|
+
Parameters
|
134
|
+
----------
|
135
|
+
section : str
|
136
|
+
A section of the configuration file.
|
137
|
+
See config_default.ini for available sections.
|
138
|
+
option : str
|
139
|
+
The option to be changed.
|
140
|
+
See config_default.ini for available options and explanations.
|
141
|
+
value : str, int, bool or list
|
142
|
+
The new value for the option.
|
143
|
+
|
144
|
+
Raises
|
145
|
+
------
|
146
|
+
PermissionError
|
147
|
+
If you try to change the database password with this method.
|
148
|
+
Use set_db_credentials instead.
|
149
|
+
"""
|
150
|
+
if "database" in section and option == "PASSWORD":
|
151
|
+
raise PermissionError(
|
152
|
+
"It is not possible to change the database password with set, please use config.set_db_credentials.")
|
153
|
+
|
154
|
+
self._set(section, option, value)
|
155
|
+
|
156
|
+
def get_list(self, section, option):
|
157
|
+
"""Get a list of values from a configuration option.
|
158
|
+
|
159
|
+
This function parses the configuration option seperated by commas and returns a list of values."""
|
160
|
+
if raw_value:= self.get(section, option, fallback=None):
|
161
|
+
return [v.strip()
|
162
|
+
for v in raw_value.replace("\n", "").split(",")
|
163
|
+
if len(v.strip())>0]
|
164
|
+
return []
|
165
|
+
|
166
|
+
def getlist(self, section, option):
|
167
|
+
"""Get a list of values from a configuration option.
|
168
|
+
|
169
|
+
This function parses the configuration option seperated by commas and returns a list of values.
|
170
|
+
|
171
|
+
Warning
|
172
|
+
-------
|
173
|
+
This function will become deprecated in the future. Please use get_list instead."""
|
174
|
+
import warnings
|
175
|
+
warnings.warn("getlist will become deprecated, please use get_list instead.", FutureWarning)
|
176
|
+
return self.get_list(section, option)
|
177
|
+
|
178
|
+
def get_datetime(self, section, option, fallback=None):
|
179
|
+
"""Get a date from a configuration option.
|
180
|
+
|
181
|
+
This function parses the configuration option and returns a datetime object."""
|
182
|
+
if raw_value:= self.get(section, option, fallback=fallback):
|
183
|
+
return datetime.strptime(raw_value, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
184
|
+
return None
|
185
|
+
|
186
|
+
def get_date(self, section, option, fallback=None):
|
187
|
+
"""Get a date from a configuration option.
|
188
|
+
|
189
|
+
This function parses the configuration option and returns a date object."""
|
190
|
+
return self.get_datetime(section, option, fallback=fallback).date()
|
191
|
+
|
192
|
+
def _get_db_key_section(self, db_key=None):
|
193
|
+
"""Get the database section for the WeatherDB database.
|
194
|
+
|
195
|
+
Parameters
|
196
|
+
----------
|
197
|
+
db_key : str, optional
|
198
|
+
The key/name for the database section in the configuration file.
|
199
|
+
If not given, the function will use the default database connection.
|
200
|
+
The default is None.
|
201
|
+
|
202
|
+
Returns
|
203
|
+
-------
|
204
|
+
str, str, configparser.SectionProxy
|
205
|
+
The connection key, keyring_key and the connection configuration section.
|
206
|
+
"""
|
207
|
+
if db_key is None:
|
208
|
+
db_key = self.get("database", "connection")
|
209
|
+
db_sect = self[f"database:{db_key}"]
|
210
|
+
return db_key, f"weatherDB_{db_sect.get('host')}", db_sect
|
211
|
+
|
212
|
+
def set_db_credentials(
|
213
|
+
self,
|
214
|
+
db_key=None,
|
215
|
+
user=None,
|
216
|
+
password=None):
|
217
|
+
"""Set the database credentials for the WeatherDB database.
|
218
|
+
|
219
|
+
Parameters
|
220
|
+
----------
|
221
|
+
db_key : str, optional
|
222
|
+
The key/name for the database section in the configuration file.
|
223
|
+
If not given, the function will use the default database connection.
|
224
|
+
The default is None.
|
225
|
+
user : str, optional
|
226
|
+
The username for the database.
|
227
|
+
If not given, the function will take the user from configuration if possible or ask for it.
|
228
|
+
The default is None.
|
229
|
+
password : str, optional
|
230
|
+
The password for the database user.
|
231
|
+
If not given, the function will ask for it.
|
232
|
+
The default is None.
|
233
|
+
"""
|
234
|
+
# get connection section and keys
|
235
|
+
db_key, keyring_key, db_sect = self._get_db_key_section(db_key=db_key)
|
236
|
+
|
237
|
+
# check if user is given
|
238
|
+
if user is None and (user:=db_sect.get("USER")) is None:
|
239
|
+
user = input(f"Please enter the username for the database '{db_sect.get('host')}\\{db_sect.get('database')}': ")
|
240
|
+
if password is None:
|
241
|
+
password = getpass(f"Please enter the password for the user {user} on the database '{db_sect.get('host')}\\{db_sect.get('database')}': ")
|
242
|
+
|
243
|
+
# test connection
|
244
|
+
try:
|
245
|
+
con_url = sa.URL.create(
|
246
|
+
drivername="postgresql+psycopg2",
|
247
|
+
username=user,
|
248
|
+
password=password,
|
249
|
+
host=db_sect["HOST"],
|
250
|
+
database=db_sect["DATABASE"],
|
251
|
+
port=db_sect["PORT"]
|
252
|
+
)
|
253
|
+
with sa.create_engine(con_url).connect() as con:
|
254
|
+
con.execute(sa.text("SELECT 1;"))
|
255
|
+
except Exception as e:
|
256
|
+
print(f"Connection failed, therefor the settings are not stored: {e}")
|
257
|
+
return
|
258
|
+
|
259
|
+
# remove old password
|
260
|
+
try:
|
261
|
+
keyring.delete_password(
|
262
|
+
keyring_key,
|
263
|
+
db_sect["USER"])
|
264
|
+
except:
|
265
|
+
pass
|
266
|
+
|
267
|
+
# set new credentials
|
268
|
+
self._set(f"database:{db_key}", "USER", user)
|
269
|
+
keyring.set_password(keyring_key, user, password)
|
270
|
+
|
271
|
+
def get_db_credentials(self, db_key=None):
|
272
|
+
"""Get the database credentials for the WeatherDB database.
|
273
|
+
|
274
|
+
Parameters
|
275
|
+
----------
|
276
|
+
db_key : str, optional
|
277
|
+
The key/name for the database section in the configuration file.
|
278
|
+
If not given, the function will use the default database connection.
|
279
|
+
The default is None.
|
280
|
+
|
281
|
+
Returns
|
282
|
+
-------
|
283
|
+
str, str
|
284
|
+
The username and the password for the database.
|
285
|
+
"""
|
286
|
+
db_key, keyring_key, db_sect = self._get_db_key_section(db_key)
|
287
|
+
|
288
|
+
if "user" not in db_sect or not keyring.get_password(keyring_key, db_sect["user"]):
|
289
|
+
print("No database credentials found. Please set them.")
|
290
|
+
self.set_db_credentials()
|
291
|
+
|
292
|
+
return db_sect["user"], keyring.get_password(keyring_key, db_sect["user"])
|
293
|
+
|
294
|
+
@property
|
295
|
+
def has_user_config(self):
|
296
|
+
"""Check if a user config file is defined.
|
297
|
+
|
298
|
+
Returns
|
299
|
+
-------
|
300
|
+
bool
|
301
|
+
True if a user config file is defined, False otherwise.
|
302
|
+
"""
|
303
|
+
return self.has_option("main", "user_config_file") or "WEATHERDB_USER_CONFIG_FILE" in os.environ
|
304
|
+
|
305
|
+
@property
|
306
|
+
def user_config_file(self):
|
307
|
+
"""Get the path to the user config file.
|
308
|
+
|
309
|
+
Returns
|
310
|
+
-------
|
311
|
+
str or None
|
312
|
+
The path to the user config file.
|
313
|
+
"""
|
314
|
+
if self.has_user_config:
|
315
|
+
if user_config := self.get("main", "user_config_file", fallback=None):
|
316
|
+
return user_config
|
317
|
+
return os.environ.get("WEATHERDB_USER_CONFIG_FILE", None)
|
318
|
+
return None
|
319
|
+
|
320
|
+
def create_user_config(self, user_config_file=None, on_exists="ask"):
|
321
|
+
"""Create a new user config file.
|
322
|
+
|
323
|
+
Parameters
|
324
|
+
----------
|
325
|
+
user_config_file : str or Path, optional
|
326
|
+
The path to the new user config file.
|
327
|
+
If not given, the function will use the config.user_config_file if available or ask for it.
|
328
|
+
If set to "ask", the function will allways open a filedialog to select the file.
|
329
|
+
The default is None.
|
330
|
+
on_exists : str, optional
|
331
|
+
What to do if the user config file already exists.
|
332
|
+
The options are:
|
333
|
+
- "ask"/"A" : Ask the user what to do.
|
334
|
+
- "overwrite"/"O" : Overwrite the existing file.
|
335
|
+
- "define"/"D" : Only define the file as new user config file location.
|
336
|
+
- "error"/"E" : Raise an error and stop the creation.
|
337
|
+
The default is "ask".
|
338
|
+
|
339
|
+
"""
|
340
|
+
if user_config_file is None:
|
341
|
+
if self.has_user_config:
|
342
|
+
user_config_file = self.user_config_file
|
343
|
+
else:
|
344
|
+
user_config_file = "ask"
|
345
|
+
|
346
|
+
# ask for the user config file
|
347
|
+
if user_config_file == "ask":
|
348
|
+
try:
|
349
|
+
from tkinter import Tk
|
350
|
+
from tkinter import filedialog
|
351
|
+
tkroot = Tk()
|
352
|
+
tkroot.attributes('-topmost', True)
|
353
|
+
tkroot.iconify()
|
354
|
+
user_config_file = filedialog.asksaveasfilename(
|
355
|
+
defaultextension=".ini",
|
356
|
+
filetypes=[("INI files", "*.ini")],
|
357
|
+
title="Where do you want to save the user config file?",
|
358
|
+
initialdir=Path("~").expanduser(),
|
359
|
+
initialfile="WeatherDB_config.ini",
|
360
|
+
confirmoverwrite=True,
|
361
|
+
)
|
362
|
+
tkroot.destroy()
|
363
|
+
except ImportError:
|
364
|
+
while True:
|
365
|
+
user_input = input("Please enter the path to the user config file: ")
|
366
|
+
if user_input.lower() in ["exit", "quit"] or user_input == "":
|
367
|
+
print("Quiting the user config creation.")
|
368
|
+
return
|
369
|
+
user_config_file = Path(user_input)
|
370
|
+
user_input = input("Please enter the path to the user config file: ")
|
371
|
+
if user_input.lower() in ["exit", "quit"] or user_input == "":
|
372
|
+
print("Quiting the user config creation.")
|
373
|
+
return
|
374
|
+
user_config_file = Path(user_input)
|
375
|
+
if user_config_file.parent.exists():
|
376
|
+
if user_config_file.suffix != ".ini":
|
377
|
+
print("The file has to be an INI file.")
|
378
|
+
continue
|
379
|
+
break
|
380
|
+
else:
|
381
|
+
print("Invalid path. Please try again.")
|
382
|
+
|
383
|
+
# check if file exists
|
384
|
+
write = True
|
385
|
+
if Path(user_config_file).exists():
|
386
|
+
msg = f"User config file already exists at {user_config_file}."
|
387
|
+
if on_exists[0].upper() == "E":
|
388
|
+
raise FileExistsError(msg)
|
389
|
+
|
390
|
+
# get input from user
|
391
|
+
print(msg)
|
392
|
+
if on_exists[0].upper() == "A":
|
393
|
+
on_exists = input(
|
394
|
+
"What do you want to do with the existing file?"+
|
395
|
+
"[overwrite/define/exit] (first letter is enough): ").upper()[0]
|
396
|
+
|
397
|
+
# treat the user input
|
398
|
+
if on_exists == "O":
|
399
|
+
write = True
|
400
|
+
elif on_exists== "D":
|
401
|
+
write = False
|
402
|
+
elif on_exists == "E":
|
403
|
+
return
|
404
|
+
else:
|
405
|
+
raise ValueError("Invalid value for on_exists. Please try again.")
|
406
|
+
|
407
|
+
# copy the default config file to the user config file
|
408
|
+
if write:
|
409
|
+
with open(user_config_file, "w") as user_f, \
|
410
|
+
open(self._DEFAULT_CONFIG_FILE, "r") as def_f:
|
411
|
+
for line in def_f.readlines():
|
412
|
+
if not re.match(r"^\[|;", line):
|
413
|
+
line = "; " + line
|
414
|
+
user_f.write(line)
|
415
|
+
print(f"User config file created at {user_config_file}")
|
416
|
+
print("Please edit the file to your needs and reload user config with load_user_config() or by reloading the module.")
|
417
|
+
|
418
|
+
# set the user config file in the main config
|
419
|
+
self._set("main", "user_config_file", str(user_config_file))
|
420
|
+
print("The user config file location got set in main config.")
|
421
|
+
|
422
|
+
def load_user_config(self,
|
423
|
+
raise_undefined_error=True,
|
424
|
+
if_not_existing=os.environ.get("WEATHERDB_HANDLE_NON_EXISTING_CONFIG", "ask")):
|
425
|
+
"""(re)load the user config file.
|
426
|
+
|
427
|
+
Parameters
|
428
|
+
----------
|
429
|
+
raise_undefined_error : bool, optional
|
430
|
+
Raise an error if no user config file is defined.
|
431
|
+
The default is True.
|
432
|
+
if_not_existing : str, optional
|
433
|
+
What to do if the user config file is not existing at the specified location.
|
434
|
+
The options are:
|
435
|
+
- "ask" : Ask the user what to do.
|
436
|
+
- "ignore" : Ignore the error and continue.
|
437
|
+
- "create" : Create a new user config file.
|
438
|
+
- "define" : Define a new user config file location.
|
439
|
+
- "remove" : Remove the user config file location.
|
440
|
+
The default is the value of the environment variable "WEATHERDB_HANDLE_NON_EXISTING_CONFIG" or if undefined "ask".
|
441
|
+
"""
|
442
|
+
if self.has_user_config:
|
443
|
+
user_config_file = self.user_config_file
|
444
|
+
if Path(user_config_file).exists():
|
445
|
+
with open(user_config_file) as f:
|
446
|
+
f_cont = f.read()
|
447
|
+
if "PASSWORD" in f_cont:
|
448
|
+
raise PermissionError(
|
449
|
+
"For security reasons the password isn't allowed to be in the config file.\nPlease use set_db_credentials to set the password.")
|
450
|
+
self.read_string(f_cont)
|
451
|
+
else:
|
452
|
+
print(f"User config file not found at {user_config_file}.")
|
453
|
+
# get user decision what to do
|
454
|
+
if if_not_existing.lower() == "ask":
|
455
|
+
print(textwrap.dedent("""
|
456
|
+
What do you want to do:
|
457
|
+
- [R] : Remove the user config file location
|
458
|
+
- [D] : Define a new user config file location
|
459
|
+
- [C] : Create a new user config file with default values
|
460
|
+
- [I] : Ignore error"""))
|
461
|
+
while True:
|
462
|
+
user_dec = input("Enter the corresponding letter: ").upper()
|
463
|
+
if user_dec in ["R", "D", "C", "I"]:
|
464
|
+
break
|
465
|
+
else:
|
466
|
+
print("Invalid input. Please try again and use one of the given letters.")
|
467
|
+
else:
|
468
|
+
user_dec = if_not_existing[0].upper()
|
469
|
+
|
470
|
+
# do the user decision
|
471
|
+
if user_dec == "R":
|
472
|
+
self.remove_option("main", "user_config_file")
|
473
|
+
elif user_dec == "D":
|
474
|
+
self.set_user_config_file()
|
475
|
+
elif user_dec == "C":
|
476
|
+
self.create_user_config()
|
477
|
+
elif raise_undefined_error:
|
478
|
+
raise FileNotFoundError("No user config file defined.")
|
479
|
+
|
480
|
+
def set_user_config_file(self, user_config_file=None):
|
481
|
+
"""Define the user config file.
|
482
|
+
|
483
|
+
Parameters
|
484
|
+
----------
|
485
|
+
user_config_file : str, Path or None, optional
|
486
|
+
The path to the user config file.
|
487
|
+
If None, the function will open a filedialog to select the file.
|
488
|
+
The default is None.
|
489
|
+
"""
|
490
|
+
if user_config_file is None:
|
491
|
+
from tkinter import Tk
|
492
|
+
from tkinter import filedialog
|
493
|
+
tkroot = Tk()
|
494
|
+
tkroot.attributes('-topmost', True)
|
495
|
+
tkroot.iconify()
|
496
|
+
user_config_file = filedialog.askopenfilename(
|
497
|
+
defaultextension=".ini",
|
498
|
+
filetypes=[("INI files", "*.ini")],
|
499
|
+
title="Select the User configuration file",
|
500
|
+
initialdir=Path("~").expanduser(),
|
501
|
+
initialfile="WeatherDB_config.ini"
|
502
|
+
)
|
503
|
+
tkroot.destroy()
|
504
|
+
|
505
|
+
if not Path(user_config_file).exists():
|
506
|
+
raise FileNotFoundError(
|
507
|
+
f"User config file not found at {user_config_file}")
|
508
|
+
|
509
|
+
self._set("main", "user_config_file", str(user_config_file))
|
510
|
+
self.load_user_config()
|
511
|
+
|
512
|
+
def update_user_config(self, section, option, value):
|
513
|
+
"""Update a specific value in the user config file.
|
514
|
+
|
515
|
+
Parameters
|
516
|
+
----------
|
517
|
+
section : str
|
518
|
+
The section of the configuration file.
|
519
|
+
option : str
|
520
|
+
The option of the configuration file.
|
521
|
+
value : str, int, bool or list
|
522
|
+
The new value for the option.
|
523
|
+
|
524
|
+
Raises
|
525
|
+
------
|
526
|
+
ValueError
|
527
|
+
If no user config file is defined.
|
528
|
+
"""
|
529
|
+
# check if user config file is defined
|
530
|
+
if not self.has_user_config:
|
531
|
+
raise ValueError("No user config file defined.\nPlease create a user config file with create_user_config() or define an existiing user configuration file with set_user_config_file, before updating the user config.")
|
532
|
+
|
533
|
+
# update the value in the config
|
534
|
+
self.set(section, option, value)
|
535
|
+
value = self.get(section, option)
|
536
|
+
|
537
|
+
# update the value in the user config file
|
538
|
+
section = section.replace(".",":").lower()
|
539
|
+
option = option.upper()
|
540
|
+
value_set = False
|
541
|
+
with open(self.user_config_file, "r") as f:
|
542
|
+
ucf_lines = f.readlines()
|
543
|
+
for commented in [False, True]:
|
544
|
+
in_section = False
|
545
|
+
for i, line in enumerate(ucf_lines):
|
546
|
+
line_c = line.strip().lower()
|
547
|
+
|
548
|
+
# get section change
|
549
|
+
if re.match(r"\[.*\]", line_c):
|
550
|
+
if in_section:
|
551
|
+
if not value_set and commented:
|
552
|
+
print(i)
|
553
|
+
print("Option not found in section and is therefor added at the end of the section.")
|
554
|
+
ucf_lines.insert(i, f"; Option added by config.update_user_config-call.\n{option} = {value}\n\n")
|
555
|
+
value_set = True
|
556
|
+
break
|
557
|
+
|
558
|
+
in_section = line_c.startswith(f"[{section}]")
|
559
|
+
|
560
|
+
# set value if option is found
|
561
|
+
if commented:
|
562
|
+
re_comp = re.compile(f"(;\\s*){option.lower()}\\s*=")
|
563
|
+
else:
|
564
|
+
re_comp = re.compile(f"{option.lower()}\\s*=")
|
565
|
+
if in_section and re_comp.match(line_c):
|
566
|
+
# check if multiline option
|
567
|
+
j = 0
|
568
|
+
while i+j<len(ucf_lines) and \
|
569
|
+
ucf_lines[i+j].strip(";").split(";")[0].strip().endswith(","):
|
570
|
+
j += 1
|
571
|
+
|
572
|
+
# remove the old additional values
|
573
|
+
for k in range(i+1, i+j+1):
|
574
|
+
ucf_lines[k] = ""
|
575
|
+
|
576
|
+
# set the value
|
577
|
+
ucf_lines[i] = f"{option} = {value}\n"
|
578
|
+
value_set = True
|
579
|
+
break
|
580
|
+
if value_set:
|
581
|
+
break
|
582
|
+
|
583
|
+
# add the option if not found in the section
|
584
|
+
if not value_set:
|
585
|
+
print("Section not found and is therefor added at the end of the file.")
|
586
|
+
ucf_lines.append(textwrap.dedent(f"""
|
587
|
+
|
588
|
+
[{section}]
|
589
|
+
; Option and section added by config.update_user_config-call.
|
590
|
+
{option} = {value}"""))
|
591
|
+
|
592
|
+
# write the new user config file
|
593
|
+
with open(self.user_config_file, "w") as f:
|
594
|
+
f.writelines(ucf_lines)
|
595
|
+
|
596
|
+
def load_environment_variables(self):
|
597
|
+
"""Load the environment variables into the configuration.
|
598
|
+
|
599
|
+
The following environment variables are possible to use:
|
600
|
+
- WEATHERDB_USER_CONFIG_FILE : The path to the user config file.
|
601
|
+
- WEATHERDB_HANDLE_NON_EXISTING_CONFIG : What to do if the user config file is not existing at the specified location.
|
602
|
+
- WEATHERDB_DB_USER : The username for the database.
|
603
|
+
- WEATHERDB_DB_PASSWORD : The password for the database user.
|
604
|
+
- WEATHERDB_DB_HOST : The host for the database.
|
605
|
+
- WEATHERDB_DB_PORT : The port for the database.
|
606
|
+
- WEATHERDB_DB_DATABASE : The database name.
|
607
|
+
- WEATHERDB_DATA_BASE_DIR : The base path for the data directory.
|
608
|
+
- WEATHERDB_LOGGING_HANDLER : The logging handler to use. Possible values are "console" and "file".
|
609
|
+
- WEATHERDB_LOGGING_LEVEL : The logging level to use. Possible values are "DEBUG", "INFO", "WARNING", "ERROR" and "CRITICAL".
|
610
|
+
- WEATHERDB_LOGGING_DIRECTORY : The directory to store the log files.
|
611
|
+
- WEATHERDB_LOGGING_FILE : The file name for the log file.
|
612
|
+
- WEATHERDB_HORIZON_RADIUS : The radius in meters for the horizon angle calculation.
|
613
|
+
- WEATHERDB_HORIZON_CRS : The CRS as EPSG code for the distance calculation during the horizon angle calculation.
|
614
|
+
"""
|
615
|
+
# database connection variables
|
616
|
+
db_vars = ["WEATHERDB_DB_USER", "WEATHERDB_DB_PASSWORD", "WEATHERDB_DB_HOST", "WEATHERDB_DB_PORT", "WEATHERDB_DB_DATABASE"]
|
617
|
+
var_exists = [var for var in db_vars if var in os.environ ]
|
618
|
+
if len(var_exists)==len(db_vars):
|
619
|
+
self.set(
|
620
|
+
"database:environment_variables",
|
621
|
+
"host",
|
622
|
+
os.environ.get("WEATHERDB_DB_HOST"))
|
623
|
+
self.set(
|
624
|
+
"database:environment_variables",
|
625
|
+
"port",
|
626
|
+
os.environ.get("WEATHERDB_DB_PORT"))
|
627
|
+
self.set(
|
628
|
+
"database:environment_variables",
|
629
|
+
"database",
|
630
|
+
os.environ.get("WEATHERDB_DB_DATABASE"))
|
631
|
+
|
632
|
+
# get password from file if it is a path, to work with docker secrets
|
633
|
+
password = os.environ.get("WEATHERDB_DB_PASSWORD")
|
634
|
+
if Path(password).exists():
|
635
|
+
with open(password, "r") as f:
|
636
|
+
password = f.read().strip()
|
637
|
+
|
638
|
+
self.set_db_credentials(
|
639
|
+
"environment_variables",
|
640
|
+
os.environ["WEATHERDB_DB_USER"],
|
641
|
+
password)
|
642
|
+
self.set("database", "connection", "environment_variables")
|
643
|
+
elif len(var_exists)>0:
|
644
|
+
print(textwrap.dedent(f"""
|
645
|
+
Only some database environment variables are set ({', '.join(var_exists)}).
|
646
|
+
To configure your database with environment variables all needed variables are needed.
|
647
|
+
Please set the following missing environment variables:"""))
|
648
|
+
for var in db_vars:
|
649
|
+
if var not in var_exists:
|
650
|
+
print(f" - {var}")
|
651
|
+
|
652
|
+
# other environment variable settings
|
653
|
+
for env_key, (section, option) in {
|
654
|
+
"WEATHERDB_DATA_BASE_DIR": ("data", "base_dir"),
|
655
|
+
"WEATHERDB_LOGGING_HANDLER": ("logging", "handlers"),
|
656
|
+
"WEATHERDB_LOGGING_LEVEL": ("logging", "level"),
|
657
|
+
"WEATHERDB_LOGGING_DIRECTORY": ("logging", "directory"),
|
658
|
+
"WEATHERDB_LOGGING_FILE": ("logging", "file"),
|
659
|
+
"WEATHERDB_HORIZON_RADIUS": ("weatherdb", "horizon_radius"),
|
660
|
+
"WEATHERDB_HORIZON_CRS": ("weatherdb", "horizon_crs"),
|
661
|
+
}.items():
|
662
|
+
if env_key in os.environ:
|
663
|
+
self.set(section, option, os.environ.get(env_key))
|