This repository was archived by the owner on Nov 2, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathconfig.py
More file actions
294 lines (237 loc) · 11.2 KB
/
config.py
File metadata and controls
294 lines (237 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
""" setting and getting the persistent configuration for pushfish-api server"""
import sys
import configparser
import os
import logging
from typing import Type, TypeVar
from collections import namedtuple
import warnings
import appdirs
ConfigOption = namedtuple("ConfigOption", ["default", "type", "required",
"envvar", "comment"])
APPNAME = "pushfish-api"
_LOGGER = logging.getLogger(APPNAME)
T = TypeVar("T", bound="Config")
def construct_default_db_uri() -> str:
dbpath = os.path.join(appdirs.user_data_dir(APPNAME), "pushfish-api.db")
return "sqlite:///" + dbpath
db_uri_comment = """#for mysql, use something like:
#uri = 'mysql+pymysql://pushfish@localhost/pushfish_api?charset=utf8mb4'"""
dispatch_zmq_comment = """#point zeromq_relay_uri at the zeromq pubsub socket for
#the pushfish connectors """
server_debug_comment = """#set debug to 0 for production mode """
DEFAULT_VALUES = {
"database": {"uri": ConfigOption(construct_default_db_uri, str, True, "PUSHFISH_DB", db_uri_comment)},
"dispatch": {"mqtt_broker_address": ConfigOption("", str, False, "MQTT_ADDRESS", None),
"google_api_key": ConfigOption("", str, False, "PUSHFISH_GOOGLE_API_KEY", None),
"google_gcm_sender_id": ConfigOption(123456789012, bool, True, "PUSHFISH_GCM_SENDER_ID", None),
"zeromq_relay_uri": ConfigOption("", str, False, "PUSHFISH_ZMQ_RELAY_URI", dispatch_zmq_comment)},
"server": {"debug": ConfigOption(0, bool, False, "PUSHFISH_DEBUG", server_debug_comment)}}
def call_if_callable(v, *args, **kwargs):
""" if v is callable, call it with args and kwargs. If not, return v itself """
return v(*args, **kwargs) if callable(v) else v
def get_config_file_path() -> str:
"""
gets a configuration file path for pushfish-api.
First, the environment variable PUSHFISH_CONFIG will be checked.
If that variable contains an invalid path, an exception is raised.
If the variable is not set, the config file will be loaded from the
platform specific standard config directory, e.g.
on linux: ~/.config/pushfish-api/pushfish-api.cfg
on Windows: C:\\Users\\user\\AppData\\Local\\pushfish-api\\pushfish-api.cfg
on OSX: /Users/user/Library/Application Support/pushfish-api/pushfish-api.cfg
The file is not created if it does not exist.
"""
# check environment variable first
cfile = os.getenv("PUSHFISH_CONFIG")
if not cfile:
_LOGGER.info("PUSHFISH_CONFIG is not set, using default config file location")
elif not os.path.exists(cfile):
_LOGGER.warning("PUSHFISH_CONFIG file path does not exist, it will be created: %s", cfile)
return cfile
else:
return cfile
configdir = appdirs.user_config_dir(appname=APPNAME)
return os.path.join(configdir, "pushfish-api.cfg")
def write_default_config(path: str = None, overwrite: bool = False):
""" writes out a config file with default options pre-loaded
Arguments:
path: the path for the config file to write. If not specified,
calls get_config_file_path() to obtain a location
overwrite: whether to overwrite an existing file. If False, and the
path already exists, raises a RuntimeError
"""
if path is None:
path = get_config_file_path()
if os.path.exists(path):
if not overwrite:
errstr = "config file {} already exists. Not overwriting".format(path)
_LOGGER.error(errstr)
raise RuntimeError(errstr)
else:
_LOGGER.warning("overwriting existing config file %s with default", path)
cfg = configparser.ConfigParser(allow_no_value=True)
for section, settings in DEFAULT_VALUES.items():
cfg.add_section(section)
for setting, value in settings.items():
v = call_if_callable(value.default)
if value.comment is not None:
cfg.set(section, value.comment)
cfg[section][setting] = str(v)
cfgdir = os.path.dirname(path)
if not os.path.exists(cfgdir):
if cfgdir:
os.mkdir(cfgdir)
with open(path, "x") as f:
cfg.write(f)
else:
with open(path, "w") as f:
cfg.write(f)
class Config:
""" reader for pushfish config file """
GLOBAL_INSTANCE = None
GLOBAL_BACKTRACE_ENABLE = False
@classmethod
def get_global_instance(cls: Type[T]) -> T:
""" returns the a global instance of the Config object.
If one has not yet been defined, raises a RuntimeError"""
if cls.GLOBAL_INSTANCE is None:
raise RuntimeError("no global config instance exists. Construct a \
Config instance somewhere in the application")
return cls.GLOBAL_INSTANCE
def __init__(self, path: str = None, create: bool = False,
overwrite: bool = False) -> None:
"""
arguments:
path: path for config file. If not specified, calls get_default_config_path()
create: create a default config file if it doesn't exist
overwrite: overwrite the config file with the default even if it
does already exist
"""
if not path:
path = get_config_file_path()
if not os.path.exists(path):
if not create:
errstr = "config file doesn't exist, and didn't pass create=True"
_LOGGER.error(errstr)
raise RuntimeError(errstr)
_LOGGER.info("config file doesn't exist, creating it...")
write_default_config(path=path, overwrite=False)
elif overwrite:
_LOGGER.warning("config file already exists, overwriting...")
write_default_config(path=path, overwrite=True)
self._cfg = configparser.ConfigParser()
with open(path, "r") as f:
self._cfg.read_file(f)
# HACK: this is purely here so that the tests can override the global app
# config
if hasattr(self, "INJECT_CONFIG"):
warnings.warn("running with injected config. If you see this \
whilst not running tests it IS AN ERROR")
self = Config.GLOBAL_INSTANCE
else:
Config.GLOBAL_INSTANCE = self
if self.debug:
Config.GLOBAL_BACKTRACE_ENABLE = True
self._check_spurious_keys()
self._load_from_env_vars()
def _load_from_env_vars(self):
for section, optdict in DEFAULT_VALUES.items():
for name, opt in optdict.items():
envval = os.getenv(opt.envvar)
if envval:
_LOGGER.info("overriding config setting %s from environment variable %s", name, opt.envvar)
try:
self._cfg[section][name] = envval
except ValueError as err:
errstr = "couldn't get value of type %s for setting %s"
fatal_error_exit_or_backtrace(err, errstr, _LOGGER, opt.type, name)
except Exception as err:
errstr = "failed to set value of setting %s from environment"
fatal_error_exit_or_backtrace(err, errstr, _LOGGER, name)
def _check_spurious_keys(self):
for section in self._cfg.sections():
if section not in DEFAULT_VALUES:
_LOGGER.critical("spurious section [%s] found in config file. ", section)
_LOGGER.critical("don't know how to handle this, exiting...")
sys.exit(1)
for key in self._cfg[section].keys():
if key not in DEFAULT_VALUES[section]:
_LOGGER.critical("spurious key %s in section [%s] found in config file. ", key, section)
_LOGGER.critical("don't know how to handle this, exiting...")
sys.exit(1)
def _safe_get_cfg_value(self, section: str, key: str):
opt = DEFAULT_VALUES[section][key]
try:
return opt.type(self._cfg[section][key])
except KeyError as err:
reportstr = "no value for REQUIRED configuration option: %s in section [%s] defined" % (key, section)
if opt.required:
fatal_error_exit_or_backtrace(err, reportstr, _LOGGER)
else:
_LOGGER.warning(reportstr)
defvalue = call_if_callable(opt.default)
_LOGGER.warning("using default value of %s", str(defvalue))
return opt.type(defvalue)
@property
def database_uri(self) -> str:
""" returns the database connection URI"""
# HACK: create directory to run db IF AND ONLY IF it's identical to
# default and doesn't exist. Please get rid of this with something
# better soon
val = self._safe_get_cfg_value("database", "uri")
if val == construct_default_db_uri():
datadb = os.path.dirname(val).split("sqlite:///")[1]
if not os.path.exists(datadb):
try:
os.mkdir(datadb)
except PermissionError as err:
errstr = "can't create default database directory. Exiting..."
fatal_error_exit_or_backtrace(err, errstr, _LOGGER)
return val
@property
def mqtt_broker_address(self) -> str:
""" returns MQTT server address"""
return self._safe_get_cfg_value("dispatch", "mqtt_broker_address")
@property
def google_api_key(self) -> str:
""" returns google API key for gcm"""
return self._safe_get_cfg_value("dispatch", "google_api_key")
@property
def google_gcm_sender_id(self) -> int:
""" returns sender id for gcm"""
return self._safe_get_cfg_value("dispatch", "google_gcm_sender_id")
@property
def zeromq_relay_uri(self) -> str:
""" returns relay URI for zeromq dispatcher"""
return self._safe_get_cfg_value("dispatch", "zeromq_relay_uri")
@property
def debug(self) -> bool:
""" returns desired debug state of application.
Overridden by the value of environment variable FLASK_DEBUG """
if int(os.getenv("FLASK_DEBUG", "0")):
return True
return self._safe_get_cfg_value("server", "debug")
def fatal_error_exit_or_backtrace(err: Exception,
msg: str,
logger: logging.Logger,
*logargs, **logkwargs):
""" standard handling of fatal errors. Logs a critical error, then, if
debug mode is enabled, rethrows the error (to get a backtrace or debug),
and if not, exits the program with return code 1
arguments:
err: the exception that caused this situation. Can be None, in which case
will not be re-raised
msg: the message you want to log
logger: the logger to log to. Can be None, in which case a default logger
will be obtained
logargs, logkwargs: arguments to pass on to the logging function
"""
if logger is None:
logger = logging.getLogger("pushfish-api")
logger.critical(msg, *logargs, **logkwargs)
logger.critical("exiting...")
if Config.GLOBAL_BACKTRACE_ENABLE:
if err is not None:
raise err
sys.exit(1)