· 7 years ago · May 31, 2018, 02:02 PM
1import os
2from librarian.utils.enums import StreamFormat, VideoFileCodec
3
4
5def string_to_bool(str_):
6 return str_ and str_.lower() not in ['false', '0']
7
8
9class ConfigError(Exception):
10 pass
11
12
13class ConfigField:
14 def __init__(self, processor=str, default=None, required=False):
15 self.processor = processor
16 self.default = default
17 self.required = required
18
19
20class EnvironConfigurationMeta(type):
21 def __call__(cls, *args, **kwargs):
22 raise Exception("Can't instance a configuration class")
23
24 def __init__(cls, name, bases, dct):
25 super(EnvironConfigurationMeta, cls).__init__(name, bases, dct)
26 if cls._load_all_from_environ:
27 for var, value in os.environ.items():
28 if not var.upper() in [k.upper() for k in dct.keys()]:
29 dct[var.upper()] = ConfigField(processor=str, default=None,
30 required=False)
31
32 for k, field in dct.items():
33 if isinstance(field, ConfigField):
34 config_val = os.environ.get(k)
35 if config_val:
36 config_val = field.processor(config_val)
37 elif field.default:
38 if callable(field.default):
39 config_val = field.default()
40 else:
41 config_val = field.default
42 if config_val is None and field.required:
43 raise ConfigError(f'Config param {k} can not be None')
44
45 setattr(cls, k, config_val)
46
47 def __getitem__(cls, item):
48 return getattr(cls, item)
49
50
51class BaseEnvironConfig(metaclass=EnvironConfigurationMeta):
52 """Base config class.
53 Loads all config params that are instances of ConfigField from environ,
54 applies preprocessor, validates if required.
55
56 `_load_all_from_environ` - if true will load all env vars as non-required
57 fields.
58
59 Example:
60 ```
61 >>> class Config(BaseEnvironConfig):
62 ... DEBUG = ConfigField(processor=string_to_bool, default=True)
63 ...
64 >>> Config.DEBUG
65 True
66 ```
67
68 Loads env var "DEBUG", applies function `string_to_bool` to the returned value,
69 if the result is None uses default of True.
70
71 Since its a global class it's __VERY IMPORTANT__ to import it like this:
72
73 ```
74 from package import module
75 print(module.ConfigClass.as_text())
76 ```
77
78 and __not__ like this:
79
80 ```
81 from package.module import ConfigClass
82 print(ConfigClass.as_text())
83 ```
84
85 The first variant allows you to monkey patch the config during testing.
86 Pytest example:
87
88 ```
89 from package.module import ConfigClass
90
91 class TestConfig(ConfigClass):
92 pass
93
94 TestConfig.TEST = True
95
96 monkeypatch.setattr('package.module.ConfigClass', TestConfig)
97 ```
98
99 After this all code accesing `module.ConfigClass` will get `TestConfig`.
100 """
101
102 _load_all_from_environ = False
103
104 @classmethod
105 def as_dict(cls):
106 non_private_attrs = [(k, v) for k, v in vars(cls).items() if
107 not k.startswith('_')]
108 data_fields = {k: v for k, v in non_private_attrs if
109 not callable(getattr(cls, k))}
110 return data_fields
111
112 @classmethod
113 def as_text(cls, exclude=[]):
114 exclude = [x.lower() for x in exclude]
115 data_fields = cls.as_dict()
116 attrs = [
117 "{}={}".format(k, str(v) if not k.lower() in exclude else '*' * len(v))
118 for k, v in data_fields.items() if v]
119 attrs.sort() # sorts normally by alphabetical order
120 attrs.sort(key=len, reverse=False) # sorts by descending length
121 return "Config:\n\t" + '\n\t'.join(attrs)
122
123
124class AkamaiConfig(BaseEnvironConfig):
125 AKAMAI_LINK_PROTECT_WINDOW = ConfigField(required=True)
126 AKAMAI_LINK_PROTECT_SALT = ConfigField(required=True)
127 AKAMAI_LINK_PROTECT_ALGO = ConfigField(required=True)
128 AKAMAI_LINK_PROTECT_KEY = ConfigField(required=True)
129 AKAMAI_LINK_PROTECT_FIELD_DELIMITER = ConfigField(required=True)
130
131
132class Configuration(BaseEnvironConfig):
133
134 _load_all_from_environ = False
135
136 # Flask configs
137 DEBUG = ConfigField(processor=string_to_bool, default=True)
138 TESTING = ConfigField(processor=string_to_bool, default=False)
139 DB_ECHO = ConfigField(processor=string_to_bool, default=False)
140 SECRET_KEY = ConfigField(default=__name__)
141 SQLALCHEMY_URL = ConfigField(required=True)
142
143 # Custom configs
144 GEO_ROUTING_FILE = ConfigField(required=True)
145 STREAM_FORMATS = ConfigField(processor=lambda var: [StreamFormat.from_string(fmt) for fmt in var.split(',')], required=True)
146 CODECS = ConfigField(processor=lambda var: [VideoFileCodec.from_string(codec) for codec in var.split(',')], required=True)
147 PREFERRED_REDIRECT_SCHEME = ConfigField(required=True, default='https')