-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathconstants.py
318 lines (233 loc) · 7.51 KB
/
constants.py
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""
Loads bot configuration from YAML files.
By default, this simply loads the default configuration located at `config-default.yml`.
If a file called `config.yml` is found in the project directory, the default configuration
is recursively updated with any settings from the custom configuration. Any settings left
out in the custom user configuration will stay their default values from `config-default.yml`.
"""
import logging
import os
from collections.abc import Mapping
from pathlib import Path
from typing import Union
import yaml
log = logging.getLogger(__name__)
def _env_var_constructor(loader, node):
"""
Implements a custom YAML tag for loading optional environment variables.
If the environment variable is set, returns the value of it.
Otherwise, returns `None`.
Example usage in the YAML configuration:
# Optional app configuration. Set `MY_APP_KEY` in the environment to use it.
application:
key: !ENV 'MY_APP_KEY'
"""
default = None
# Check if the node is a plain string value
if node.id == "scalar":
value = loader.construct_scalar(node)
key = str(value)
else:
# The node value is a list
value = loader.construct_sequence(node)
if len(value) >= 2:
# If we have at least two values, then we have both a key and a default value
default = value[1]
key = value[0]
else:
# Otherwise, we just have a key
key = value[0]
return os.getenv(key, default)
def _join_var_constructor(loader, node):
"""Implements a custom YAML tag for concatenating other tags in the document to strings.
This allows for a much more DRY configuration file.
"""
fields = loader.construct_sequence(node)
return "".join(str(x) for x in fields)
yaml.SafeLoader.add_constructor("!ENV", _env_var_constructor)
yaml.SafeLoader.add_constructor("!JOIN", _join_var_constructor)
with open("config-default.yml", encoding="UTF-8") as file:
_CONFIG_YAML = yaml.safe_load(file)
def _recursive_update(original, new):
"""
Helper method which implements a recursive `dict.update` method, used for updating the
original configuration with configuration specified by the user.
"""
for key, value in original.items():
if key not in new:
continue
if isinstance(value, Mapping):
if not any(isinstance(subvalue, Mapping) for subvalue in value.values()):
original[key].update(new[key])
_recursive_update(original[key], new[key])
else:
original[key] = new[key]
if Path("config.yml").exists():
# Overwriting default config with new config.
log.info("Found `config.yml` file, loading constants from it.")
with open("config.yml", encoding="UTF-8") as file:
user_config = yaml.safe_load(file)
_recursive_update(_CONFIG_YAML, user_config)
def check_required_keys(keys):
"""
Verifies that keys that are set to be required are present in the
loaded configuration.
"""
for key_path in keys:
lookup = _CONFIG_YAML
try:
for key in key_path.split("."):
lookup = lookup[key]
if lookup is None:
raise KeyError(key)
except KeyError:
log.critical(
f"A configuration for `{key_path}` is required, but was not found. "
"Please set it in `config.yml` or setup an environment variable and try again."
)
raise
try:
required_keys = _CONFIG_YAML["config"]["required_keys"]
except KeyError:
pass
else:
check_required_keys(required_keys)
class YAMLGetter(type):
"""
Implements a custom metaclass used for accessing
configuration data by simply accessing class attributes.
Supports getting configuration from up to two levels
of nested configuration through `section` and `subsection`.
`section` specifies the YAML configuration section (or "key")
in which the configuration lives, and must be set.
`subsection` is an optional attribute specifying the section
within the section from which configuration should be loaded.
Example Usage:
# config.yml
bot:
prefixes:
direct_message: ''
guild: '!'
# config.py
class Prefixes(metaclass=YAMLGetter):
section = "bot"
subsection = "prefixes"
# Usage in Python code
from config import Prefixes
def get_prefix(bot, message):
if isinstance(message.channel, PrivateChannel):
return Prefixes.direct_message
return Prefixes.guild
"""
subsection = None
def __getattr__(cls, name):
name = name.lower()
try:
if cls.subsection is not None:
return _CONFIG_YAML[cls.section][cls.subsection][name]
return _CONFIG_YAML[cls.section][name]
except KeyError:
dotted_path = ".".join(
(cls.section, cls.subsection, name)
if cls.subsection is not None
else (cls.section, name)
)
log.critical(
f"Tried accessing configuration variable at `{dotted_path}`, but it could not be found."
)
raise
def __getitem__(cls, name):
return cls.__getattr__(name)
def __iter__(cls):
"""Return generator of key: value pairs of current constants class' config values."""
for name in cls.__annotations__:
yield name, getattr(cls, name)
# Dataclasses
class Bot(metaclass=YAMLGetter):
"""Type hints of `config.yml` "bot"."""
section = "bot"
prefix: str
token: str
id: int
log_level: Union[str, int]
class Db(metaclass=YAMLGetter):
section = "db"
host: str
user: str
password: str
class Sentry(metaclass=YAMLGetter):
section = "sentry"
dsn_key: str
class Finnhub(metaclass=YAMLGetter):
section = "finnhub"
token: str
url: str
class Shop_emoji(metaclass=YAMLGetter):
section = "shop"
subsection = "emoji"
buy_price: int
delete_price: int
class Colours(metaclass=YAMLGetter):
section = "style"
subsection = "colours"
soft_red: int
soft_green: int
soft_orange: int
bright_green: int
class Emojis(metaclass=YAMLGetter):
section = "style"
subsection = "emojis"
status_online: str
status_offline: str
status_idle: str
status_dnd: str
incident_actioned: str
incident_unactioned: str
incident_investigating: str
bullet: str
new: str
pencil: str
cross_mark: str
check_mark: str
first: str
previous: str
next: str
last: str
close: str
class Icons(metaclass=YAMLGetter):
section = "style"
subsection = "icons"
crown_blurple: str
crown_green: str
crown_red: str
defcon_denied: str
defcon_disabled: str
defcon_enabled: str
defcon_updated: str
filtering: str
hash_blurple: str
hash_green: str
hash_red: str
message_bulk_delete: str
message_delete: str
message_edit: str
sign_in: str
sign_out: str
user_ban: str
user_unban: str
user_update: str
user_mute: str
user_unmute: str
user_verified: str
user_warn: str
pencil: str
remind_blurple: str
remind_green: str
remind_red: str
questionmark: str
voice_state_blue: str
voice_state_green: str
voice_state_red: str
# Paths
BOT_DIR = os.path.dirname(__file__)
PROJECT_ROOT = os.path.abspath(os.path.join(BOT_DIR, os.pardir))