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