weatherdb 1.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. docker/Dockerfile +30 -0
  2. docker/docker-compose.yaml +58 -0
  3. docker/docker-compose_test.yaml +24 -0
  4. docker/start-docker-test.sh +6 -0
  5. docs/requirements.txt +10 -0
  6. docs/source/Changelog.md +2 -0
  7. docs/source/License.rst +7 -0
  8. docs/source/Methode.md +161 -0
  9. docs/source/_static/custom.css +8 -0
  10. docs/source/_static/favicon.ico +0 -0
  11. docs/source/_static/logo.png +0 -0
  12. docs/source/api/api.rst +15 -0
  13. docs/source/api/cli.rst +8 -0
  14. docs/source/api/weatherDB.broker.rst +10 -0
  15. docs/source/api/weatherDB.config.rst +7 -0
  16. docs/source/api/weatherDB.db.rst +23 -0
  17. docs/source/api/weatherDB.rst +22 -0
  18. docs/source/api/weatherDB.station.rst +56 -0
  19. docs/source/api/weatherDB.stations.rst +46 -0
  20. docs/source/api/weatherDB.utils.rst +22 -0
  21. docs/source/conf.py +137 -0
  22. docs/source/index.rst +33 -0
  23. docs/source/setup/Configuration.md +127 -0
  24. docs/source/setup/Hosting.md +9 -0
  25. docs/source/setup/Install.md +49 -0
  26. docs/source/setup/Quickstart.md +183 -0
  27. docs/source/setup/setup.rst +12 -0
  28. weatherdb/__init__.py +24 -0
  29. weatherdb/_version.py +1 -0
  30. weatherdb/alembic/README.md +8 -0
  31. weatherdb/alembic/alembic.ini +80 -0
  32. weatherdb/alembic/config.py +9 -0
  33. weatherdb/alembic/env.py +100 -0
  34. weatherdb/alembic/script.py.mako +26 -0
  35. weatherdb/alembic/versions/V1.0.0_initial_database_creation.py +898 -0
  36. weatherdb/alembic/versions/V1.0.2_more_charachters_for_settings+term_station_ma_raster.py +88 -0
  37. weatherdb/alembic/versions/V1.0.5_fix-ma-raster-values.py +152 -0
  38. weatherdb/alembic/versions/V1.0.6_update-views.py +22 -0
  39. weatherdb/broker.py +667 -0
  40. weatherdb/cli.py +214 -0
  41. weatherdb/config/ConfigParser.py +663 -0
  42. weatherdb/config/__init__.py +5 -0
  43. weatherdb/config/config_default.ini +162 -0
  44. weatherdb/db/__init__.py +3 -0
  45. weatherdb/db/connections.py +374 -0
  46. weatherdb/db/fixtures/RichterParameters.json +34 -0
  47. weatherdb/db/models.py +402 -0
  48. weatherdb/db/queries/get_quotient.py +155 -0
  49. weatherdb/db/views.py +165 -0
  50. weatherdb/station/GroupStation.py +710 -0
  51. weatherdb/station/StationBases.py +3108 -0
  52. weatherdb/station/StationET.py +111 -0
  53. weatherdb/station/StationP.py +807 -0
  54. weatherdb/station/StationPD.py +98 -0
  55. weatherdb/station/StationT.py +164 -0
  56. weatherdb/station/__init__.py +13 -0
  57. weatherdb/station/constants.py +21 -0
  58. weatherdb/stations/GroupStations.py +519 -0
  59. weatherdb/stations/StationsBase.py +1021 -0
  60. weatherdb/stations/StationsBaseTET.py +30 -0
  61. weatherdb/stations/StationsET.py +17 -0
  62. weatherdb/stations/StationsP.py +128 -0
  63. weatherdb/stations/StationsPD.py +24 -0
  64. weatherdb/stations/StationsT.py +21 -0
  65. weatherdb/stations/__init__.py +11 -0
  66. weatherdb/utils/TimestampPeriod.py +369 -0
  67. weatherdb/utils/__init__.py +3 -0
  68. weatherdb/utils/dwd.py +350 -0
  69. weatherdb/utils/geometry.py +69 -0
  70. weatherdb/utils/get_data.py +285 -0
  71. weatherdb/utils/logging.py +126 -0
  72. weatherdb-1.1.0.dist-info/LICENSE +674 -0
  73. weatherdb-1.1.0.dist-info/METADATA +765 -0
  74. weatherdb-1.1.0.dist-info/RECORD +77 -0
  75. weatherdb-1.1.0.dist-info/WHEEL +5 -0
  76. weatherdb-1.1.0.dist-info/entry_points.txt +2 -0
  77. weatherdb-1.1.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,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))
@@ -0,0 +1,5 @@
1
+ from .ConfigParser import ConfigParser
2
+
3
+ config = ConfigParser()
4
+
5
+ __all__ = ["config"]